├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1.bug.md │ ├── 2.feature.md │ └── config.yml └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile-devel ├── LICENSE ├── Makefile ├── README.md ├── cmd └── chirpstack-gateway-bridge │ ├── cmd │ ├── configfile.go │ ├── root.go │ ├── root_run.go │ ├── root_run_syslog.go │ ├── root_run_syslog_stub.go │ └── version.go │ └── main.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── backend │ ├── backend.go │ ├── basicstation │ │ ├── backend.go │ │ ├── backend_test.go │ │ ├── gateway.go │ │ ├── metrics.go │ │ └── structs │ │ │ ├── downlink_message.go │ │ │ ├── downlink_message_test.go │ │ │ ├── downlink_transmitted.go │ │ │ ├── eui64.go │ │ │ ├── eui64_test.go │ │ │ ├── join_request.go │ │ │ ├── join_request_test.go │ │ │ ├── msg_type.go │ │ │ ├── msg_type_test.go │ │ │ ├── radio_meta_data.go │ │ │ ├── radio_meta_data_test.go │ │ │ ├── router_config.go │ │ │ ├── router_config_test.go │ │ │ ├── router_info.go │ │ │ ├── time_sync.go │ │ │ ├── uplink_data_frame.go │ │ │ ├── uplink_data_frame_test.go │ │ │ ├── uplink_proprietary.go │ │ │ ├── uplink_proprietary_test.go │ │ │ └── version.go │ ├── concentratord │ │ ├── concentratord.go │ │ ├── concentratord_test.go │ │ └── metrics.go │ ├── events │ │ └── events.go │ ├── semtechudp │ │ ├── backend.go │ │ ├── backend_test.go │ │ ├── metrics.go │ │ ├── packets │ │ │ ├── packets.go │ │ │ ├── packets_test.go │ │ │ ├── packettype_string.go │ │ │ ├── pull_ack.go │ │ │ ├── pull_ack_test.go │ │ │ ├── pull_data.go │ │ │ ├── pull_data_test.go │ │ │ ├── pull_resp.go │ │ │ ├── pull_resp_test.go │ │ │ ├── push_ack.go │ │ │ ├── push_ack_test.go │ │ │ ├── push_data.go │ │ │ ├── push_data_test.go │ │ │ ├── tx_ack.go │ │ │ └── tx_ack_test.go │ │ ├── registry.go │ │ └── test │ │ │ └── test.json │ └── stats │ │ ├── stats.go │ │ └── stats_test.go ├── commands │ ├── commands.go │ └── commands_test.go ├── config │ ├── config.go │ └── sx1301v1 │ │ ├── sx1301v1.go │ │ └── sx1301v1_test.go ├── filters │ ├── filters.go │ └── filters_test.go ├── forwarder │ └── forwarder.go ├── integration │ ├── integration.go │ └── mqtt │ │ ├── auth │ │ ├── auth.go │ │ ├── azure_iot_hub.go │ │ ├── azure_iot_hub_test.go │ │ ├── gcp_cloud_iot_core.go │ │ ├── generic.go │ │ └── generic_test.go │ │ ├── backend.go │ │ ├── backend_test.go │ │ └── metrics.go ├── metadata │ ├── metadata.go │ └── metadata_test.go ├── metrics │ └── metrics.go └── tools │ └── tools.go └── packaging ├── files ├── chirpstack-gateway-bridge.init ├── chirpstack-gateway-bridge.rotate ├── chirpstack-gateway-bridge.service └── chirpstack-gateway-bridge.toml ├── scripts ├── compress-mips.sh ├── post-install.sh ├── post-remove.sh └── pre-install.sh └── vendor ├── cisco └── IXM-LPWA │ ├── files │ ├── MANIFEST │ ├── VERSION_SDK │ ├── etc │ │ ├── chirpstack-gateway-bridge │ │ │ └── chirpstack-gateway-bridge.toml │ │ ├── init.d │ │ │ ├── chirpstack-gateway-bridge │ │ │ └── lora-packet-forwarder │ │ └── lora-packet-forwarder │ │ │ └── config.json │ └── scripts │ │ ├── POSTINSTALL │ │ ├── POSTUNINSTALL │ │ ├── PREUNINSTALL │ │ ├── RESTART │ │ ├── START │ │ ├── STATUS │ │ ├── STOP │ │ └── VERSION │ └── package.sh ├── dragino └── LG308 │ ├── files │ ├── chirpstack-gateway-bridge.init │ └── chirpstack-gateway-bridge.toml │ └── package.sh ├── kerlink └── keros-gws │ ├── files │ ├── chirpstack-gateway-bridge.init │ ├── chirpstack-gateway-bridge.monit │ └── chirpstack-gateway-bridge.toml │ └── package.sh ├── multitech └── conduit │ ├── files │ ├── chirpstack-gateway-bridge.init │ ├── chirpstack-gateway-bridge.monit │ └── chirpstack-gateway-bridge.toml │ └── package.sh └── tektelic └── kona ├── files ├── chirpstack-gateway-bridge.init ├── chirpstack-gateway-bridge.monit └── chirpstack-gateway-bridge.toml └── package.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: chirpstack 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report for ChirpStack Gateway Bridge 4 | --- 5 | 6 | 10 | 11 | 12 | 13 | - [ ] The issue is present in the latest release. 14 | - [ ] I have searched the [issues](https://github.com/brocaar/chirpstack-gateway-bridge) of this repository and believe that this is not a duplicate. 15 | 16 | ## What happened? 17 | 18 | ## What did you expect? 19 | 20 | ## Steps to reproduce this issue 21 | 22 | Steps: 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 4. 28 | 29 | ## Could you share your log output? 30 | 31 | 36 | ```shell 37 | 38 | ``` 39 | 40 | ## Your Environment 41 | 42 | 59 | 60 | 61 | | Component | Version | 62 | | --------------------| ------- | 63 | | Application Server | v?.?.? | 64 | | Network Server | | 65 | | Gateway Bridge | | 66 | | Chirpstack API | | 67 | | Geolocation | | 68 | | Concentratord | | 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2.feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new idea to ChirpStack Gateway Bridge 4 | --- 5 | 6 | 10 | 11 | 12 | 13 | - [ ] I have searched the [issues](https://github.com/brocaar/chirpstack-gateway-bridge) of this repository and believe that this is not a duplicate. 14 | 15 | ## Summary 16 | 17 | 18 | 19 | ## What is the use-case? 20 | 21 | 22 | 23 | ## Implementation description 24 | 25 | 28 | 29 | ## Can you implement this by yourself and make a pull request? 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false # force the usage of a template 2 | contact_links: 3 | - name: ChirpStack Community Forum 4 | url: https://forum.chirpstack.io/ 5 | about: I need support with ChirpStack. 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | tags: 7 | - 'v*' 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v2 17 | - 18 | name: Run tests 19 | run: docker compose run --rm chirpstack-gateway-bridge make test 20 | 21 | dist: 22 | needs: test 23 | runs-on: ubuntu-latest 24 | if: startsWith(github.ref, 'refs/tags/v') 25 | steps: 26 | - 27 | name: Checkout 28 | uses: actions/checkout@v2 29 | with: 30 | fetch-depth: 0 31 | - 32 | name: Configure AWS credentials 33 | uses: aws-actions/configure-aws-credentials@v1 34 | with: 35 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 36 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 37 | aws-region: eu-west-1 38 | - 39 | name: Build distributable binaries 40 | run: docker compose run --rm chirpstack-gateway-bridge make dist 41 | - 42 | name: Upload binaries to S3 43 | if: startsWith(github.ref, 'refs/tags/v') 44 | run: | 45 | aws s3 sync dist/upload s3://builds.loraserver.io/chirpstack-gateway-bridge 46 | 47 | # Runs on pull request to ensure the docker build works, 48 | # but pushes only on commits to master and on PRs. 49 | docker: 50 | needs: test 51 | runs-on: ubuntu-latest 52 | steps: 53 | - 54 | name: Checkout 55 | uses: actions/checkout@v2 56 | with: 57 | fetch-depth: 0 58 | - 59 | name: Docker meta 60 | id: meta 61 | uses: docker/metadata-action@v3 62 | with: 63 | images: | 64 | chirpstack/${{ github.event.repository.name }} 65 | tags: | 66 | type=semver,pattern={{version}} 67 | type=semver,pattern={{major}} 68 | type=semver,pattern={{major}}.{{minor}} 69 | - 70 | name: Set up QEMU 71 | uses: docker/setup-qemu-action@v1 72 | - 73 | name: Set up Docker Buildx 74 | uses: docker/setup-buildx-action@v1 75 | - 76 | name: Login to DockerHub 77 | if: startsWith(github.ref, 'refs/tags/v') 78 | uses: docker/login-action@v1 79 | with: 80 | username: ${{ secrets.DOCKERHUB_USERNAME }} 81 | password: ${{ secrets.DOCKERHUB_TOKEN }} 82 | - 83 | name: Build and push 84 | id: docker_build 85 | uses: docker/build-push-action@v2 86 | with: 87 | platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 88 | push: ${{ startsWith(github.ref, 'refs/tags/v') }} 89 | tags: ${{ steps.meta.outputs.tags }} 90 | labels: ${{ steps.meta.outputs.labels }} 91 | - 92 | name: Image digest 93 | run: echo ${{ steps.docker_build.outputs.digest }} 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # hidden files 2 | .* 3 | 4 | # Vagrant 5 | Vagrantfile 6 | 7 | # configuration files 8 | /*.toml 9 | 10 | # coverage file 11 | /coverage.out 12 | 13 | # certificates 14 | /certs 15 | 16 | # binaries 17 | /dist 18 | /build 19 | /docs/public 20 | *.ipk 21 | 22 | # build folders 23 | /packaging/vendor/*/*/package 24 | /packaging/vendor/*/*/temp 25 | /packaging/vendor/*/*/*.tar.gz 26 | /packaging/vendor/*/*/key 27 | 28 | # dependencies 29 | /vendor 30 | 31 | # certificates 32 | /certs 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/themes/chirpstack-hugo-theme"] 2 | path = docs/themes/chirpstack-hugo-theme 3 | url = https://github.com/brocaar/chirpstack-hugo-theme.git 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: chirpstack-gateway-bridge 2 | 3 | env: 4 | - GOMIPS=softfloat 5 | 6 | builds: 7 | - main: cmd/chirpstack-gateway-bridge/main.go 8 | binary: chirpstack-gateway-bridge 9 | goos: 10 | - linux 11 | goarch: 12 | - amd64 13 | - arm 14 | - arm64 15 | - mips 16 | goarm: 17 | - 5 18 | - 6 19 | - 7 20 | hooks: 21 | post: ./packaging/scripts/compress-mips.sh 22 | 23 | release: 24 | disable: true 25 | 26 | nfpm: 27 | vendor: ChirpStack 28 | homepage: https://www.chirpstack.io/ 29 | maintainer: Orne Brocaar 30 | description: ChirpStack Gateway Bridge 31 | license: MIT 32 | formats: 33 | - deb 34 | - rpm 35 | bindir: /usr/bin 36 | files: 37 | "packaging/files/chirpstack-gateway-bridge.rotate": "/etc/logrotate.d/chirpstack-gateway-bridge" 38 | "packaging/files/chirpstack-gateway-bridge.init": "/usr/lib/chirpstack-gateway-bridge/scripts/chirpstack-gateway-bridge.init" 39 | "packaging/files/chirpstack-gateway-bridge.service": "/usr/lib/chirpstack-gateway-bridge/scripts/chirpstack-gateway-bridge.service" 40 | config_files: 41 | "packaging/files/chirpstack-gateway-bridge.toml": "/etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml" 42 | scripts: 43 | preinstall: "packaging/scripts/pre-install.sh" 44 | postinstall: "packaging/scripts/post-install.sh" 45 | postremove: "packaging/scripts/post-remove.sh" 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | There are a couple of ways to get involved: 4 | 5 | * Join the discussions: 6 | * LoRa Server project forum [https://forum.chirpstack.io/](https://forum.chirpstack.io/) 7 | * Report bugs or make feature-requests by opening an issue at [https://github.com/brocaar/chirpstack-gateway-bridge/issues](https://github.com/brocaar/chirpstack-gateway-bridge/issues) 8 | * Help fixing issues or improve documentation by creating pull-requests 9 | 10 | 11 | When you would like to add new features, please discuss the feature first 12 | by creating an issue describing your feature, how you're planning to implement 13 | it, what the usecase is etc... 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.1-alpine AS development 2 | 3 | ENV PROJECT_PATH=/chirpstack-gateway-bridge 4 | ENV PATH=$PATH:$PROJECT_PATH/build 5 | ENV CGO_ENABLED=0 6 | ENV GO_EXTRA_BUILD_ARGS="-a -installsuffix cgo" 7 | 8 | RUN apk add --no-cache ca-certificates make git bash 9 | 10 | RUN mkdir -p $PROJECT_PATH 11 | COPY . $PROJECT_PATH 12 | WORKDIR $PROJECT_PATH 13 | 14 | RUN make dev-requirements 15 | RUN make 16 | 17 | FROM alpine:3.17.0 AS production 18 | 19 | RUN apk --no-cache add ca-certificates 20 | COPY --from=development /chirpstack-gateway-bridge/build/chirpstack-gateway-bridge /usr/bin/chirpstack-gateway-bridge 21 | USER nobody:nogroup 22 | ENTRYPOINT ["/usr/bin/chirpstack-gateway-bridge"] 23 | -------------------------------------------------------------------------------- /Dockerfile-devel: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.1-alpine 2 | 3 | ENV PROJECT_PATH=/chirpstack-gateway-bridge 4 | ENV PATH=$PATH:$PROJECT_PATH/build 5 | ENV CGO_ENABLED=0 6 | ENV GO_EXTRA_BUILD_ARGS="-a -installsuffix cgo" 7 | 8 | RUN apk add --no-cache ca-certificates make git bash upx rpm tar 9 | 10 | RUN mkdir -p $PROJECT_PATH 11 | COPY . $PROJECT_PATH 12 | WORKDIR $PROJECT_PATH 13 | 14 | RUN git config --global --add safe.directory /chirpstack-gateway-bridge 15 | RUN make dev-requirements 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Orne Brocaar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean test package serve run-compose-test 2 | VERSION := $(shell git describe --always |sed -e "s/^v//") 3 | 4 | build: 5 | @echo "Compiling source" 6 | @mkdir -p build 7 | go build $(GO_EXTRA_BUILD_ARGS) -ldflags "-s -w -X main.version=$(VERSION)" -o build/chirpstack-gateway-bridge cmd/chirpstack-gateway-bridge/main.go 8 | 9 | clean: 10 | @echo "Cleaning up workspace" 11 | @rm -rf build 12 | @rm -rf dist 13 | 14 | test: 15 | @echo "Running tests" 16 | @rm -f coverage.out 17 | @golint ./... 18 | @go vet ./... 19 | @go test -cover -v -coverprofile coverage.out -p 1 ./... 20 | 21 | dist: 22 | @goreleaser 23 | mkdir -p dist/upload/tar 24 | mkdir -p dist/upload/deb 25 | mkdir -p dist/upload/rpm 26 | mv dist/*.tar.gz dist/upload/tar 27 | mv dist/*.deb dist/upload/deb 28 | mv dist/*.rpm dist/upload/rpm 29 | 30 | snapshot: 31 | @goreleaser --snapshot 32 | 33 | dev-requirements: 34 | go install golang.org/x/lint/golint 35 | go install github.com/goreleaser/goreleaser 36 | go install github.com/goreleaser/nfpm 37 | 38 | # shortcuts for development 39 | 40 | serve: build 41 | ./build/chirpstack-gateway-bridge 42 | 43 | run-compose-test: 44 | docker compose run --rm chirpstack-gateway-bridge make test 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChirpStack Gateway Bridge 2 | 3 | ![Tests](https://github.com/brocaar/chirpstack-gateway-bridge/actions/workflows/main.yml/badge.svg?branch=master) 4 | 5 | ChirpStack Gateway Bridge is a service which converts LoRa® Packet Forwarder protocols 6 | into a ChirpStack [common data-format](https://github.com/chirpstack/chirpstack/blob/master/api/proto/gw/gw.proto) (JSON and Protobuf). 7 | This component is part of the ChirpStack open-source LoRaWAN® Network Server project. 8 | 9 | ## Backends 10 | 11 | The following packet-forwarder backends are provided: 12 | 13 | * [Semtech UDP packet-forwarder](https://github.com/Lora-net/packet_forwarder) 14 | * [Basic Station packet-forwarder](https://github.com/lorabasics/basicstation) 15 | * [ChirpStack Concentratord](https://github.com/brocaar/chirpstack-concentratord/) 16 | 17 | ## Integrations 18 | 19 | The following integrations are provided: 20 | 21 | * Generic MQTT broker 22 | * [GCP Cloud IoT Core MQTT Bridge](https://cloud.google.com/iot-core/) 23 | * [Azure IoT Hub](https://azure.microsoft.com/en-us/services/iot-hub/) 24 | 25 | ## Documentation 26 | 27 | Please refer to the [ChirpStack documentation](https://www.chirpstack.io/) for 28 | more information. 29 | 30 | ## License 31 | 32 | ChirpStack Gateway Bridge is distributed under the MIT license. See 33 | [LICENSE](https://github.com/brocaar/chirpstack-gateway-bridge/blob/master/LICENSE). 34 | -------------------------------------------------------------------------------- /cmd/chirpstack-gateway-bridge/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "time" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | 15 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 16 | ) 17 | 18 | var cfgFiles *[]string // config file 19 | var version string 20 | 21 | var rootCmd = &cobra.Command{ 22 | Use: "chirpstack-gateway-bridge", 23 | Short: "abstracts the packet_forwarder protocol into Protobuf or JSON over MQTT", 24 | Long: `ChirpStack Gateway Bridge abstracts the packet_forwarder protocol into Protobuf or JSON over MQTT 25 | > documentation & support: https://www.chirpstack.io/ 26 | > source & copyright information: https://github.com/chirpstack/chirpstack-gateway-bridge`, 27 | RunE: run, 28 | } 29 | 30 | func init() { 31 | cobra.OnInitialize(initConfig) 32 | 33 | cfgFiles = rootCmd.PersistentFlags().StringSliceP("config", "c", []string{}, "path to configuration file (optional)") 34 | rootCmd.PersistentFlags().Int("log-level", 4, "debug=5, info=4, error=2, fatal=1, panic=0") 35 | 36 | viper.BindPFlag("general.log_level", rootCmd.PersistentFlags().Lookup("log-level")) 37 | 38 | // default values 39 | viper.SetDefault("general.log_level", 4) 40 | viper.SetDefault("backend.type", "semtech_udp") 41 | viper.SetDefault("backend.semtech_udp.udp_bind", "0.0.0.0:1700") 42 | viper.SetDefault("backend.semtech_udp.cleanup_duration", time.Minute) 43 | 44 | viper.SetDefault("backend.concentratord.crc_check", true) 45 | viper.SetDefault("backend.concentratord.event_url", "ipc:///tmp/concentratord_event") 46 | viper.SetDefault("backend.concentratord.command_url", "ipc:///tmp/concentratord_command") 47 | 48 | viper.SetDefault("backend.basic_station.bind", ":3001") 49 | viper.SetDefault("backend.basic_station.stats_interval", time.Second*30) 50 | viper.SetDefault("backend.basic_station.ping_interval", time.Minute) 51 | viper.SetDefault("backend.basic_station.read_timeout", time.Minute+(5*time.Second)) 52 | viper.SetDefault("backend.basic_station.write_timeout", time.Second) 53 | viper.SetDefault("backend.basic_station.region", "EU868") 54 | viper.SetDefault("backend.basic_station.frequency_min", 863000000) 55 | viper.SetDefault("backend.basic_station.frequency_max", 870000000) 56 | 57 | viper.SetDefault("integration.marshaler", "protobuf") 58 | viper.SetDefault("integration.mqtt.auth.type", "generic") 59 | 60 | viper.SetDefault("integration.mqtt.event_topic_template", "gateway/{{ .GatewayID }}/event/{{ .EventType }}") 61 | viper.SetDefault("integration.mqtt.state_topic_template", "gateway/{{ .GatewayID }}/state/{{ .StateType }}") 62 | viper.SetDefault("integration.mqtt.command_topic_template", "gateway/{{ .GatewayID }}/command/#") 63 | viper.SetDefault("integration.mqtt.state_retained", true) 64 | viper.SetDefault("integration.mqtt.keep_alive", 30*time.Second) 65 | viper.SetDefault("integration.mqtt.max_reconnect_interval", time.Minute) 66 | viper.SetDefault("integration.mqtt.max_token_wait", time.Minute) 67 | 68 | viper.SetDefault("integration.mqtt.auth.generic.servers", []string{"tcp://127.0.0.1:1883"}) 69 | viper.SetDefault("integration.mqtt.auth.generic.clean_session", true) 70 | 71 | viper.SetDefault("integration.mqtt.auth.gcp_cloud_iot_core.server", "ssl://mqtt.googleapis.com:8883") 72 | viper.SetDefault("integration.mqtt.auth.gcp_cloud_iot_core.jwt_expiration", time.Hour*24) 73 | 74 | viper.SetDefault("integration.mqtt.auth.azure_iot_hub.sas_token_expiration", 24*time.Hour) 75 | 76 | viper.SetDefault("meta_data.dynamic.split_delimiter", "=") 77 | viper.SetDefault("meta_data.dynamic.execution_interval", time.Minute) 78 | viper.SetDefault("meta_data.dynamic.max_execution_duration", time.Second) 79 | 80 | rootCmd.AddCommand(versionCmd) 81 | rootCmd.AddCommand(configCmd) 82 | } 83 | 84 | // Execute executes the root command. 85 | func Execute(v string) { 86 | version = v 87 | if err := rootCmd.Execute(); err != nil { 88 | log.Fatal(err) 89 | } 90 | } 91 | 92 | func initConfig() { 93 | if cfgFiles != nil && len(*cfgFiles) != 0 { 94 | var filesMerged []byte 95 | for _, cfgFile := range *cfgFiles { 96 | cfgFileContent, err := ioutil.ReadFile(cfgFile) 97 | if err != nil { 98 | log.WithError(err).WithField("config", cfgFile).Fatal("error loading config file") 99 | } 100 | filesMerged = bytes.Join([][]byte{ 101 | filesMerged, 102 | cfgFileContent, 103 | }, []byte("\n")) 104 | } 105 | 106 | viper.SetConfigType("toml") 107 | if err := viper.ReadConfig(bytes.NewBuffer(filesMerged)); err != nil { 108 | log.WithError(err).WithField("config", cfgFiles).Fatal("error loading config file") 109 | } 110 | } else { 111 | viper.SetConfigName("chirpstack-gateway-bridge") 112 | viper.AddConfigPath(".") 113 | viper.AddConfigPath("$HOME/.config/chirpstack-gateway-bridge") 114 | viper.AddConfigPath("/etc/chirpstack-gateway-bridge/") 115 | if err := viper.ReadInConfig(); err != nil { 116 | switch err.(type) { 117 | case viper.ConfigFileNotFoundError: 118 | default: 119 | log.WithError(err).Fatal("read configuration file error") 120 | } 121 | } 122 | } 123 | 124 | for _, pair := range os.Environ() { 125 | d := strings.SplitN(pair, "=", 2) 126 | if strings.Contains(d[0], ".") { 127 | log.Warning("Using dots in env variable is illegal and deprecated. Please use double underscore `__` for: ", d[0]) 128 | underscoreName := strings.ReplaceAll(d[0], ".", "__") 129 | // Set only when the underscore version doesn't already exist. 130 | if _, exists := os.LookupEnv(underscoreName); !exists { 131 | os.Setenv(underscoreName, d[1]) 132 | } 133 | } 134 | } 135 | 136 | viperBindEnvs(config.C) 137 | 138 | if err := viper.Unmarshal(&config.C); err != nil { 139 | log.WithError(err).Fatal("unmarshal config error") 140 | } 141 | 142 | // migrate server to servers 143 | if config.C.Integration.MQTT.Auth.Generic.Server != "" { 144 | config.C.Integration.MQTT.Auth.Generic.Servers = []string{config.C.Integration.MQTT.Auth.Generic.Server} 145 | } 146 | } 147 | 148 | func viperBindEnvs(iface interface{}, parts ...string) { 149 | ifv := reflect.ValueOf(iface) 150 | ift := reflect.TypeOf(iface) 151 | for i := 0; i < ift.NumField(); i++ { 152 | v := ifv.Field(i) 153 | t := ift.Field(i) 154 | tv, ok := t.Tag.Lookup("mapstructure") 155 | if !ok { 156 | tv = strings.ToLower(t.Name) 157 | } 158 | if tv == "-" { 159 | continue 160 | } 161 | 162 | switch v.Kind() { 163 | case reflect.Struct: 164 | viperBindEnvs(v.Interface(), append(parts, tv)...) 165 | default: 166 | // Bash doesn't allow env variable names with a dot so 167 | // bind the double underscore version. 168 | keyDot := strings.Join(append(parts, tv), ".") 169 | keyUnderscore := strings.Join(append(parts, tv), "__") 170 | viper.BindEnv(keyDot, strings.ToUpper(keyUnderscore)) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /cmd/chirpstack-gateway-bridge/cmd/root_run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/pkg/errors" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend" 13 | "github.com/brocaar/chirpstack-gateway-bridge/internal/commands" 14 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 15 | "github.com/brocaar/chirpstack-gateway-bridge/internal/filters" 16 | "github.com/brocaar/chirpstack-gateway-bridge/internal/forwarder" 17 | "github.com/brocaar/chirpstack-gateway-bridge/internal/integration" 18 | "github.com/brocaar/chirpstack-gateway-bridge/internal/metadata" 19 | "github.com/brocaar/chirpstack-gateway-bridge/internal/metrics" 20 | ) 21 | 22 | func run(cmd *cobra.Command, args []string) error { 23 | 24 | tasks := []func() error{ 25 | setLogJSON, 26 | setLogLevel, 27 | setSyslog, 28 | printStartMessage, 29 | setupFilters, 30 | setupBackend, 31 | setupIntegration, 32 | setupForwarder, 33 | setupMetrics, 34 | setupMetaData, 35 | setupCommands, 36 | startIntegration, 37 | startBackend, 38 | } 39 | 40 | for _, t := range tasks { 41 | if err := t(); err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | 46 | sigChan := make(chan os.Signal, 1) 47 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 48 | log.WithField("signal", <-sigChan).Info("signal received") 49 | log.Warning("shutting down server") 50 | 51 | integration.GetIntegration().Stop() 52 | 53 | return nil 54 | } 55 | 56 | func setLogJSON() error { 57 | if config.C.General.LogJSON { 58 | log.SetFormatter(&log.JSONFormatter{}) 59 | } 60 | return nil 61 | } 62 | 63 | func setLogLevel() error { 64 | log.SetLevel(log.Level(uint8(config.C.General.LogLevel))) 65 | return nil 66 | } 67 | 68 | func printStartMessage() error { 69 | log.WithFields(log.Fields{ 70 | "version": version, 71 | "docs": "https://www.chirpstack.io/gateway-bridge/", 72 | }).Info("starting ChirpStack Gateway Bridge") 73 | return nil 74 | } 75 | 76 | func setupBackend() error { 77 | if err := backend.Setup(config.C); err != nil { 78 | return errors.Wrap(err, "setup backend error") 79 | } 80 | return nil 81 | } 82 | 83 | func setupIntegration() error { 84 | if err := integration.Setup(config.C); err != nil { 85 | return errors.Wrap(err, "setup integration error") 86 | } 87 | return nil 88 | } 89 | 90 | func setupForwarder() error { 91 | if err := forwarder.Setup(config.C); err != nil { 92 | return errors.Wrap(err, "setup forwarder error") 93 | } 94 | return nil 95 | } 96 | 97 | func setupMetrics() error { 98 | if err := metrics.Setup(config.C); err != nil { 99 | return errors.Wrap(err, "setup metrics error") 100 | } 101 | return nil 102 | } 103 | 104 | func setupMetaData() error { 105 | if err := metadata.Setup(config.C); err != nil { 106 | return errors.Wrap(err, "setup meta-data error") 107 | } 108 | return nil 109 | } 110 | 111 | func setupFilters() error { 112 | if err := filters.Setup(config.C); err != nil { 113 | return errors.Wrap(err, "setup filters error") 114 | } 115 | return nil 116 | } 117 | 118 | func setupCommands() error { 119 | if err := commands.Setup(config.C); err != nil { 120 | return errors.Wrap(err, "setup commands error") 121 | } 122 | return nil 123 | } 124 | 125 | func startIntegration() error { 126 | if err := integration.GetIntegration().Start(); err != nil { 127 | return errors.Wrap(err, "start integration error") 128 | } 129 | return nil 130 | } 131 | 132 | func startBackend() error { 133 | if err := backend.GetBackend().Start(); err != nil { 134 | return errors.Wrap(err, "start backend error") 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /cmd/chirpstack-gateway-bridge/cmd/root_run_syslog.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package cmd 4 | 5 | import ( 6 | "log/syslog" 7 | 8 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | lsyslog "github.com/sirupsen/logrus/hooks/syslog" 12 | ) 13 | 14 | func setSyslog() error { 15 | if !config.C.General.LogToSyslog { 16 | return nil 17 | } 18 | 19 | var prio syslog.Priority 20 | 21 | switch log.StandardLogger().Level { 22 | case log.DebugLevel: 23 | prio = syslog.LOG_USER | syslog.LOG_DEBUG 24 | case log.InfoLevel: 25 | prio = syslog.LOG_USER | syslog.LOG_INFO 26 | case log.WarnLevel: 27 | prio = syslog.LOG_USER | syslog.LOG_WARNING 28 | case log.ErrorLevel: 29 | prio = syslog.LOG_USER | syslog.LOG_ERR 30 | case log.FatalLevel: 31 | prio = syslog.LOG_USER | syslog.LOG_CRIT 32 | case log.PanicLevel: 33 | prio = syslog.LOG_USER | syslog.LOG_CRIT 34 | } 35 | 36 | hook, err := lsyslog.NewSyslogHook("", "", prio, "chirpstack-gateway-bridge") 37 | if err != nil { 38 | return errors.Wrap(err, "get syslog hook error") 39 | } 40 | 41 | log.AddHook(hook) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /cmd/chirpstack-gateway-bridge/cmd/root_run_syslog_stub.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func setSyslog() error { 11 | if config.C.General.LogToSyslog { 12 | log.Fatal("syslog logging is not supported on Windows") 13 | } 14 | 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /cmd/chirpstack-gateway-bridge/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var versionCmd = &cobra.Command{ 10 | Use: "version", 11 | Short: "Print the ChirpStack Gateway Bridge version", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | fmt.Println(version) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cmd/chirpstack-gateway-bridge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/chirpstack-gateway-bridge/cmd/chirpstack-gateway-bridge/cmd" 7 | paho "github.com/eclipse/paho.mqtt.golang" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type pahoLogWrapper struct { 12 | ln func(...interface{}) 13 | f func(string, ...interface{}) 14 | } 15 | 16 | func (d pahoLogWrapper) Println(v ...interface{}) { 17 | d.ln(v...) 18 | } 19 | 20 | func (d pahoLogWrapper) Printf(format string, v ...interface{}) { 21 | d.f(format, v...) 22 | } 23 | 24 | func enableClientLogging() { 25 | l := log.WithField("module", "mqtt") 26 | paho.DEBUG = pahoLogWrapper{l.Debugln, l.Debugf} 27 | paho.ERROR = pahoLogWrapper{l.Errorln, l.Errorf} 28 | paho.WARN = pahoLogWrapper{l.Warningln, l.Warningf} 29 | paho.CRITICAL = pahoLogWrapper{l.Errorln, l.Errorf} 30 | } 31 | 32 | func init() { 33 | log.SetFormatter(&log.TextFormatter{ 34 | TimestampFormat: time.RFC3339Nano, 35 | }) 36 | 37 | enableClientLogging() 38 | } 39 | 40 | var version string // set by the compiler 41 | 42 | func main() { 43 | cmd.Execute(version) 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | chirpstack-gateway-bridge: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-devel 7 | ports: 8 | - "1700:1700/udp" 9 | volumes: 10 | - ./:/chirpstack-gateway-bridge 11 | links: 12 | - mosquitto 13 | environment: 14 | - MQTT_SERVER=tcp://mosquitto:1883 15 | - TEST_MQTT_SERVER=tcp://mosquitto:1883 16 | 17 | mosquitto: 18 | image: eclipse-mosquitto:1.6 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brocaar/chirpstack-gateway-bridge 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.4 6 | 7 | require ( 8 | github.com/brocaar/lorawan v0.0.0-20230609081225-559f55342122 9 | github.com/chirpstack/chirpstack/api/go/v4 v4.6.0 10 | github.com/eclipse/paho.mqtt.golang v1.4.3 11 | github.com/go-zeromq/zmq4 v0.16.0 12 | github.com/golang-jwt/jwt/v4 v4.5.0 13 | github.com/goreleaser/goreleaser v0.106.0 14 | github.com/goreleaser/nfpm v0.11.0 15 | github.com/gorilla/websocket v1.5.1 16 | github.com/patrickmn/go-cache v2.1.0+incompatible 17 | github.com/pkg/errors v0.9.1 18 | github.com/prometheus/client_golang v1.19.0 19 | github.com/sirupsen/logrus v1.9.3 20 | github.com/spf13/cobra v1.8.0 21 | github.com/spf13/viper v1.18.2 22 | github.com/stretchr/testify v1.8.4 23 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 24 | google.golang.org/protobuf v1.33.0 25 | ) 26 | 27 | require ( 28 | github.com/Masterminds/semver v1.4.2 // indirect 29 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 30 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 31 | github.com/apex/log v1.1.0 // indirect 32 | github.com/aws/aws-sdk-go v1.34.0 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2 // indirect 35 | github.com/caarlos0/ctrlc v1.0.0 // indirect 36 | github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e // indirect 37 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 39 | github.com/fatih/color v1.14.1 // indirect 40 | github.com/fsnotify/fsnotify v1.7.0 // indirect 41 | github.com/go-zeromq/goczmq/v4 v4.2.2 // indirect 42 | github.com/golang/protobuf v1.5.4 // indirect 43 | github.com/google/go-github v17.0.0+incompatible // indirect 44 | github.com/google/go-querystring v1.0.0 // indirect 45 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect 46 | github.com/hashicorp/hcl v1.0.0 // indirect 47 | github.com/imdario/mergo v0.3.6 // indirect 48 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 49 | github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 // indirect 50 | github.com/jmespath/go-jmespath v0.3.0 // indirect 51 | github.com/kamilsk/retry/v4 v4.0.0 // indirect 52 | github.com/magiconair/properties v1.8.7 // indirect 53 | github.com/mattn/go-colorable v0.1.13 // indirect 54 | github.com/mattn/go-isatty v0.0.17 // indirect 55 | github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 // indirect 56 | github.com/mitchellh/go-homedir v1.1.0 // indirect 57 | github.com/mitchellh/mapstructure v1.5.0 // indirect 58 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect 59 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 60 | github.com/prometheus/client_model v0.6.0 // indirect 61 | github.com/prometheus/common v0.50.0 // indirect 62 | github.com/prometheus/procfs v0.13.0 // indirect 63 | github.com/sagikazarmark/locafero v0.4.0 // indirect 64 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 65 | github.com/smartystreets/assertions v1.0.0 // indirect 66 | github.com/sourcegraph/conc v0.3.0 // indirect 67 | github.com/spf13/afero v1.11.0 // indirect 68 | github.com/spf13/cast v1.6.0 // indirect 69 | github.com/spf13/pflag v1.0.5 // indirect 70 | github.com/subosito/gotenv v1.6.0 // indirect 71 | go.uber.org/multierr v1.11.0 // indirect 72 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect 73 | golang.org/x/net v0.22.0 // indirect 74 | golang.org/x/oauth2 v0.18.0 // indirect 75 | golang.org/x/sync v0.6.0 // indirect 76 | golang.org/x/sys v0.18.0 // indirect 77 | golang.org/x/text v0.14.0 // indirect 78 | golang.org/x/tools v0.18.0 // indirect 79 | google.golang.org/appengine v1.6.7 // indirect 80 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect 81 | gopkg.in/ini.v1 v1.67.0 // indirect 82 | gopkg.in/yaml.v2 v2.4.0 // indirect 83 | gopkg.in/yaml.v3 v3.0.1 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /internal/backend/backend.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/basicstation" 9 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/concentratord" 10 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/events" 11 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/semtechudp" 12 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 13 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 14 | ) 15 | 16 | var backend Backend 17 | 18 | // Setup configures the backend. 19 | func Setup(conf config.Config) error { 20 | var err error 21 | 22 | switch conf.Backend.Type { 23 | case "semtech_udp": 24 | backend, err = semtechudp.NewBackend(conf) 25 | case "basic_station": 26 | backend, err = basicstation.NewBackend(conf) 27 | case "concentratord": 28 | backend, err = concentratord.NewBackend(conf) 29 | default: 30 | return fmt.Errorf("unknown backend type: %s", conf.Backend.Type) 31 | } 32 | 33 | if err != nil { 34 | return errors.Wrap(err, "new backend error") 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // GetBackend returns the backend. 41 | func GetBackend() Backend { 42 | return backend 43 | } 44 | 45 | // Backend defines the interface that a backend must implement 46 | type Backend interface { 47 | // Stop closes the backend. 48 | Stop() error 49 | 50 | // Start starts the backend. 51 | Start() error 52 | 53 | // SetDownlinkTxAckFunc sets the DownlinkTXAck handler func. 54 | SetDownlinkTxAckFunc(func(*gw.DownlinkTxAck)) 55 | 56 | // SetGatewayStatsFunc sets the GatewayStats handler func. 57 | SetGatewayStatsFunc(func(*gw.GatewayStats)) 58 | 59 | // SetUplinkFrameFunc sets the UplinkFrame handler func. 60 | SetUplinkFrameFunc(func(*gw.UplinkFrame)) 61 | 62 | // SetRawPacketForwarderEventFunc sets the RawPacketForwarderEvent handler func. 63 | SetRawPacketForwarderEventFunc(func(*gw.RawPacketForwarderEvent)) 64 | 65 | // SetSubscribeEventFunc sets the Subscribe handler func. 66 | SetSubscribeEventFunc(func(events.Subscribe)) 67 | 68 | // SendDownlinkFrame sends the given downlink frame. 69 | SendDownlinkFrame(*gw.DownlinkFrame) error 70 | 71 | // ApplyConfiguration applies the given configuration to the gateway. 72 | ApplyConfiguration(*gw.GatewayConfiguration) error 73 | 74 | // RawPacketForwarderCommand sends the given raw command to the packet-forwarder. 75 | RawPacketForwarderCommand(*gw.RawPacketForwarderCommand) error 76 | } 77 | -------------------------------------------------------------------------------- /internal/backend/basicstation/gateway.go: -------------------------------------------------------------------------------- 1 | package basicstation 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | 10 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/events" 11 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/stats" 12 | "github.com/brocaar/lorawan" 13 | ) 14 | 15 | var ( 16 | errGatewayDoesNotExist = errors.New("gateway does not exist") 17 | ) 18 | 19 | type connection struct { 20 | sync.Mutex 21 | conn *websocket.Conn 22 | stats *stats.Collector 23 | lastTimesync time.Time 24 | } 25 | 26 | type gateways struct { 27 | sync.RWMutex 28 | gateways map[lorawan.EUI64]*connection 29 | 30 | subscribeEventFunc func(events.Subscribe) 31 | } 32 | 33 | func (g *gateways) get(id lorawan.EUI64) (*connection, error) { 34 | g.RLock() 35 | defer g.RUnlock() 36 | 37 | gw, ok := g.gateways[id] 38 | if !ok { 39 | return gw, errGatewayDoesNotExist 40 | } 41 | return gw, nil 42 | } 43 | 44 | func (g *gateways) set(id lorawan.EUI64, c *connection) error { 45 | g.Lock() 46 | defer g.Unlock() 47 | 48 | g.gateways[id] = c 49 | 50 | if g.subscribeEventFunc != nil { 51 | g.subscribeEventFunc(events.Subscribe{Subscribe: true, GatewayID: id}) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (g *gateways) getLastTimesync(id lorawan.EUI64) (time.Time, error) { 58 | g.RLock() 59 | defer g.RUnlock() 60 | 61 | gw, ok := g.gateways[id] 62 | if !ok { 63 | return time.Time{}, errGatewayDoesNotExist 64 | } 65 | 66 | return gw.lastTimesync, nil 67 | } 68 | 69 | func (g *gateways) setLastTimesync(id lorawan.EUI64, ts time.Time) error { 70 | g.Lock() 71 | defer g.Unlock() 72 | 73 | gw, ok := g.gateways[id] 74 | if !ok { 75 | return errGatewayDoesNotExist 76 | } 77 | 78 | gw.lastTimesync = ts 79 | g.gateways[id] = gw 80 | 81 | return nil 82 | } 83 | 84 | func (g *gateways) remove(id lorawan.EUI64) error { 85 | g.Lock() 86 | defer g.Unlock() 87 | 88 | if g.subscribeEventFunc != nil { 89 | g.subscribeEventFunc(events.Subscribe{Subscribe: false, GatewayID: id}) 90 | } 91 | 92 | delete(g.gateways, id) 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/backend/basicstation/metrics.go: -------------------------------------------------------------------------------- 1 | package basicstation 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | ppc = promauto.NewCounterVec(prometheus.CounterOpts{ 10 | Name: "backend_basicstation_websocket_ping_pong_count", 11 | Help: "The number of WebSocket Ping/Pong requests sent and received (per event type).", 12 | }, []string{"type"}) 13 | 14 | wsr = promauto.NewCounterVec(prometheus.CounterOpts{ 15 | Name: "backend_basicstation_websocket_received_count", 16 | Help: "The number of WebSocket messages received by the backend (per msgtype).", 17 | }, []string{"msgtype"}) 18 | 19 | wss = promauto.NewCounterVec(prometheus.CounterOpts{ 20 | Name: "backend_basicstation_websocket_sent_count", 21 | Help: "The number of WebSocket messages sent by the backend (per msgtype).", 22 | }, []string{"msgtype"}) 23 | 24 | gwc = prometheus.NewCounter(prometheus.CounterOpts{ 25 | Name: "backend_basicstation_gateway_connect_count", 26 | Help: "The number of gateway connections received by the backend.", 27 | }) 28 | 29 | gwd = prometheus.NewCounter(prometheus.CounterOpts{ 30 | Name: "backend_basicstation_gateway_disconnect_count", 31 | Help: "The number of gateways that disconnected from the backend.", 32 | }) 33 | ) 34 | 35 | func websocketPingPongCounter(typ string) prometheus.Counter { 36 | return ppc.With(prometheus.Labels{"type": typ}) 37 | } 38 | 39 | func websocketReceiveCounter(msgtype string) prometheus.Counter { 40 | return wsr.With(prometheus.Labels{"msgtype": msgtype}) 41 | } 42 | 43 | func websocketSendCounter(msgtype string) prometheus.Counter { 44 | return wss.With(prometheus.Labels{"msgtype": msgtype}) 45 | } 46 | 47 | func connectCounter() prometheus.Counter { 48 | return gwc 49 | } 50 | 51 | func disconnectCounter() prometheus.Counter { 52 | return gwd 53 | } 54 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/downlink_message.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "github.com/brocaar/lorawan/band" 11 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 12 | ) 13 | 14 | // DownlinkFrame implements the downlink message. 15 | type DownlinkFrame struct { 16 | MessageType MessageType `json:"msgtype"` 17 | 18 | DevEui string `json:"DevEui"` 19 | DC int `json:"dC"` 20 | DIID uint32 `json:"diid"` 21 | PDU string `json:"pdu"` 22 | Priority int `json:"priority"` 23 | RxDelay *int `json:"RxDelay,omitempty"` 24 | RX1DR *int `json:"RX1DR,omitempty"` 25 | RX1Freq *uint32 `json:"RX1Freq,omitempty"` 26 | RX2DR *int `json:"RX2DR,omitempty"` 27 | RX2Freq *uint32 `json:"RX2Freq,omitempty"` 28 | DR *int `json:"DR,omitempty"` 29 | Freq *uint32 `json:"Freq,omitempty"` 30 | GPSTime *uint64 `json:"gpstime,omitempty"` 31 | XTime *uint64 `json:"xtime,omitempty"` 32 | RCtx *uint64 `json:"rctx,omitempty"` 33 | MuxTime *float64 `json:"MuxTime,omitempty"` 34 | } 35 | 36 | // DownlinkFrameFromProto convers the given protobuf message to a DownlinkFrame. 37 | func DownlinkFrameFromProto(loraBand band.Band, pb *gw.DownlinkFrame) (DownlinkFrame, error) { 38 | if len(pb.Items) == 0 { 39 | return DownlinkFrame{}, errors.New("items must contain at least one item") 40 | } 41 | 42 | // MuxTime 43 | muxTime := float64(time.Now().UnixMicro()) / 1000000 44 | 45 | // We assume this is for RX1 46 | item := pb.Items[0] 47 | 48 | out := DownlinkFrame{ 49 | MessageType: DownlinkMessage, 50 | Priority: 1, // not (yet) available through gw.DownlinkFrame 51 | DevEui: "01-01-01-01-01-01-01-01", // set to fake DevEUI (setting it to 0 causes the BasicStation to not send acks, see https://github.com/lorabasics/basicstation/issues/71). 52 | DIID: pb.DownlinkId, 53 | PDU: hex.EncodeToString(item.PhyPayload), 54 | MuxTime: &muxTime, 55 | } 56 | 57 | // context 58 | // depending the scheduling type, there might or might not be a context 59 | if len(item.GetTxInfo().Context) >= 8 { 60 | var rctx, xtime uint64 61 | rctx = binary.BigEndian.Uint64(item.GetTxInfo().Context[0:8]) 62 | xtime = binary.BigEndian.Uint64(item.GetTxInfo().Context[8:16]) 63 | 64 | out.RCtx = &rctx 65 | out.XTime = &xtime 66 | } 67 | 68 | // get data-rate 69 | var dr int 70 | var err error 71 | 72 | modulation := item.GetTxInfo().GetModulation() 73 | if lora := modulation.GetLora(); lora != nil { 74 | dr, err = loraBand.GetDataRateIndex(false, band.DataRate{ 75 | Modulation: band.LoRaModulation, 76 | SpreadFactor: int(lora.SpreadingFactor), 77 | Bandwidth: int(lora.Bandwidth / 1000), 78 | }) 79 | if err != nil { 80 | return out, errors.Wrap(err, "get data-rate index error") 81 | } 82 | } 83 | 84 | if fsk := modulation.GetFsk(); fsk != nil { 85 | dr, err = loraBand.GetDataRateIndex(false, band.DataRate{ 86 | Modulation: band.FSKModulation, 87 | BitRate: int(fsk.Datarate), 88 | }) 89 | if err != nil { 90 | return out, errors.Wrap(err, "get data-rate index error") 91 | } 92 | } 93 | 94 | timing := item.GetTxInfo().GetTiming() 95 | if immediately := timing.GetImmediately(); immediately != nil { 96 | out.DC = 2 // Class-C 97 | out.RX2DR = &dr 98 | out.RX2Freq = &item.GetTxInfo().Frequency 99 | } 100 | 101 | if delay := timing.GetDelay(); delay != nil { 102 | delay := int(delay.Delay.AsDuration() / time.Second) 103 | 104 | out.DC = 0 // Class-A 105 | out.RxDelay = &delay 106 | out.RX1DR = &dr 107 | out.RX1Freq = &item.GetTxInfo().Frequency 108 | } 109 | 110 | if gpsEpoch := timing.GetGpsEpoch(); gpsEpoch != nil { 111 | gpsEpoch := uint64(gpsEpoch.TimeSinceGpsEpoch.AsDuration() / time.Microsecond) 112 | 113 | out.DC = 1 // Class-B 114 | out.DR = &dr 115 | out.Freq = &item.GetTxInfo().Frequency 116 | out.GPSTime = &gpsEpoch 117 | } 118 | 119 | // We assume this is the RX2. 120 | if len(pb.Items) == 2 { 121 | item := pb.Items[1] 122 | 123 | if delay := item.GetTxInfo().GetTiming().GetDelay(); delay != nil { 124 | modulation := item.GetTxInfo().GetModulation() 125 | 126 | if lora := modulation.GetLora(); lora != nil { 127 | dr, err := loraBand.GetDataRateIndex(false, band.DataRate{ 128 | Modulation: band.LoRaModulation, 129 | SpreadFactor: int(lora.SpreadingFactor), 130 | Bandwidth: int(lora.Bandwidth / 1000), 131 | }) 132 | if err != nil { 133 | return out, errors.Wrap(err, "get data-rate index error") 134 | } 135 | 136 | out.RX2Freq = &item.GetTxInfo().Frequency 137 | out.RX2DR = &dr 138 | } 139 | 140 | if fsk := modulation.GetFsk(); fsk != nil { 141 | dr, err := loraBand.GetDataRateIndex(false, band.DataRate{ 142 | Modulation: band.FSKModulation, 143 | BitRate: int(fsk.Datarate), 144 | }) 145 | if err != nil { 146 | return out, errors.Wrap(err, "get data-rate index error") 147 | } 148 | 149 | out.RX2Freq = &item.GetTxInfo().Frequency 150 | out.RX2DR = &dr 151 | } 152 | } 153 | } 154 | 155 | return out, nil 156 | } 157 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/downlink_transmitted.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "github.com/brocaar/lorawan" 5 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 6 | ) 7 | 8 | // DownlinkTransmitted implements the downlink transmitted message. 9 | type DownlinkTransmitted struct { 10 | MessageType MessageType `json:"msgtype"` 11 | 12 | DIID uint32 `json:"diid"` 13 | } 14 | 15 | // DownlinkTransmittedToProto converts the DownlinkTransmitted to the protobuf struct. 16 | func DownlinkTransmittedToProto(gatewayID lorawan.EUI64, dt DownlinkTransmitted) (gw.DownlinkTxAck, error) { 17 | return gw.DownlinkTxAck{ 18 | GatewayId: gatewayID.String(), 19 | DownlinkId: dt.DIID, 20 | Items: []*gw.DownlinkTxAckItem{ 21 | { 22 | Status: gw.TxAckStatus_OK, 23 | }, 24 | }, 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/eui64.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/brocaar/lorawan" 12 | ) 13 | 14 | var euiRegexp = regexp.MustCompile(`\w{2}-\w{2}-\w{2}-\w{2}-\w{2}-\w{2}-\w{2}-\w{2}`) 15 | 16 | // EUI64 implements the BasicStation EUI64 type. 17 | type EUI64 lorawan.EUI64 18 | 19 | // MarshalText encodes the EUI64 to a ID6 string. 20 | func (e EUI64) MarshalText() ([]byte, error) { 21 | return []byte(fmt.Sprintf("%x:%x:%x:%x", e[0:2], e[2:4], e[4:6], e[6:8])), nil 22 | } 23 | 24 | // UnmarshalText decodes the EUI64 from an ID6 or EUI string. 25 | func (e *EUI64) UnmarshalText(text []byte) error { 26 | v := string(text) 27 | var eui lorawan.EUI64 28 | 29 | if euiRegexp.MatchString(v) { 30 | v = strings.Replace(v, "-", "", -1) 31 | if err := eui.UnmarshalText([]byte(v)); err != nil { 32 | return errors.Wrap(err, "unmarshal eui error") 33 | } 34 | } else { 35 | var blockI int 36 | blocks := strings.Split(v, ":") 37 | for i := 0; i < len(blocks); { 38 | if blocks[i] == "" { 39 | remaining := remainingBlocks(blocks[i:]) 40 | i = len(blocks) - remaining 41 | blockI = 4 - remaining 42 | } else { 43 | v := "0000"[len(blocks[i]):] + blocks[i] 44 | b, err := hex.DecodeString(v) 45 | if err != nil { 46 | return errors.Wrap(err, "unmarshal eui block error") 47 | } 48 | for ii, bb := range b { 49 | eui[(blockI*2)+ii] = bb 50 | } 51 | 52 | blockI++ 53 | i++ 54 | } 55 | } 56 | } 57 | 58 | *e = EUI64(eui) 59 | return nil 60 | } 61 | 62 | func remainingBlocks(blocks []string) int { 63 | var i int 64 | for _, v := range blocks { 65 | if v != "" { 66 | i++ 67 | } 68 | } 69 | return i 70 | } 71 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/eui64_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestStringToEUI64(t *testing.T) { 10 | assert := require.New(t) 11 | 12 | tests := []struct { 13 | Value string 14 | Expected EUI64 15 | Error error 16 | }{ 17 | { 18 | Value: "::0", 19 | Expected: EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 20 | }, 21 | { 22 | Value: "1::", 23 | Expected: EUI64{0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 24 | }, 25 | { 26 | Value: "::a:b", 27 | Expected: EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0b}, 28 | }, 29 | { 30 | Value: "f::1", 31 | Expected: EUI64{0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, 32 | }, 33 | { 34 | Value: "f:a123:f8:100", 35 | Expected: EUI64{0x00, 0x0f, 0xa1, 0x23, 0x00, 0xf8, 0x01, 0x00}, 36 | }, 37 | { 38 | Value: "01-02-03-04-05-06-07-08", 39 | Expected: EUI64{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 40 | }, 41 | } 42 | 43 | for _, tst := range tests { 44 | var eui EUI64 45 | assert.Equal(tst.Error, eui.UnmarshalText([]byte(tst.Value))) 46 | assert.Equal(tst.Expected, eui) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/join_request.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/brocaar/lorawan" 9 | "github.com/brocaar/lorawan/band" 10 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 11 | ) 12 | 13 | // JoinRequest implements the join-request message. 14 | type JoinRequest struct { 15 | RadioMetaData 16 | 17 | MessageType MessageType `json:"msgType"` 18 | MHDR uint8 `json:"Mhdr"` 19 | JoinEUI EUI64 `json:"JoinEui"` 20 | DevEUI EUI64 `json:"DevEui"` 21 | DevNonce uint16 `json:"DevNonce"` 22 | MIC int32 `json:"MIC"` 23 | } 24 | 25 | // JoinRequestToProto converts the JoinRequest to the protobuf struct. 26 | func JoinRequestToProto(loraBand band.Band, gatewayID lorawan.EUI64, jr JoinRequest) (*gw.UplinkFrame, error) { 27 | var pb gw.UplinkFrame 28 | if err := SetRadioMetaDataToProto(loraBand, gatewayID, jr.RadioMetaData, &pb); err != nil { 29 | return &pb, errors.Wrap(err, "set radio meta-data error") 30 | } 31 | 32 | // MHDR 33 | pb.PhyPayload = append(pb.PhyPayload, jr.MHDR) 34 | 35 | // JoinEUI (little endian) 36 | joinEUI := make([]byte, len(jr.JoinEUI)) 37 | for i := 0; i < len(jr.JoinEUI); i++ { 38 | joinEUI[len(jr.JoinEUI)-1-i] = jr.JoinEUI[i] 39 | } 40 | pb.PhyPayload = append(pb.PhyPayload, joinEUI...) 41 | 42 | // DevEUI (little endian) 43 | devEUI := make([]byte, len(jr.DevEUI)) 44 | for i := 0; i < len(jr.DevEUI); i++ { 45 | devEUI[len(jr.DevEUI)-1-i] = jr.DevEUI[i] 46 | } 47 | pb.PhyPayload = append(pb.PhyPayload, devEUI...) 48 | 49 | // DevNonce 50 | devNonce := make([]byte, 2) 51 | binary.LittleEndian.PutUint16(devNonce, jr.DevNonce) 52 | pb.PhyPayload = append(pb.PhyPayload, devNonce...) 53 | 54 | // MIC 55 | mic := make([]byte, 4) 56 | binary.LittleEndian.PutUint32(mic, uint32(jr.MIC)) 57 | pb.PhyPayload = append(pb.PhyPayload, mic...) 58 | 59 | return &pb, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/join_request_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | "google.golang.org/protobuf/types/known/durationpb" 9 | "google.golang.org/protobuf/types/known/timestamppb" 10 | 11 | "github.com/brocaar/lorawan" 12 | "github.com/brocaar/lorawan/band" 13 | "github.com/brocaar/lorawan/gps" 14 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 15 | ) 16 | 17 | func TestJoinRequestToProto(t *testing.T) { 18 | assert := require.New(t) 19 | b, err := band.GetConfig(band.EU868, false, lorawan.DwellTimeNoLimit) 20 | assert.NoError(err) 21 | 22 | pTime := timestamppb.New(time.Time(gps.NewTimeFromTimeSinceGPSEpoch(5 * time.Second))) 23 | 24 | jr := JoinRequest{ 25 | RadioMetaData: RadioMetaData{ 26 | DR: 5, 27 | Frequency: 868100000, 28 | UpInfo: RadioMetaDataUpInfo{ 29 | RCtx: 1, 30 | XTime: 2, 31 | GPSTime: int64(5 * time.Second / time.Microsecond), 32 | RSSI: 120, 33 | SNR: 5.5, 34 | }, 35 | }, 36 | 37 | MessageType: JoinRequestMessage, 38 | MHDR: 0x00, 39 | JoinEUI: EUI64{0x02, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 40 | DevEUI: EUI64{0x03, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 41 | DevNonce: 20, 42 | MIC: -10, 43 | } 44 | 45 | pb, err := JoinRequestToProto(b, lorawan.EUI64{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, jr) 46 | assert.NoError(err) 47 | 48 | assert.Equal(&gw.UplinkFrame{ 49 | PhyPayload: []byte{0x00, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x02, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x03, 0x14, 0x00, 0xf6, 0xff, 0xff, 0xff}, 50 | TxInfo: &gw.UplinkTxInfo{ 51 | Frequency: 868100000, 52 | Modulation: &gw.Modulation{ 53 | Parameters: &gw.Modulation_Lora{ 54 | Lora: &gw.LoraModulationInfo{ 55 | Bandwidth: 125000, 56 | SpreadingFactor: 7, 57 | CodeRate: gw.CodeRate_CR_4_5, 58 | }, 59 | }, 60 | }, 61 | }, 62 | RxInfo: &gw.UplinkRxInfo{ 63 | GatewayId: "0102030405060708", 64 | GwTime: pTime, 65 | TimeSinceGpsEpoch: durationpb.New(5 * time.Second), 66 | Rssi: 120, 67 | Snr: 5.5, 68 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 69 | CrcStatus: gw.CRCStatus_CRC_OK, 70 | }, 71 | }, pb) 72 | 73 | } 74 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/msg_type.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // MessageType defines the message type. 10 | type MessageType string 11 | 12 | // Message types. 13 | const ( 14 | VersionMessage MessageType = "version" 15 | RouterConfigMessage MessageType = "router_config" 16 | JoinRequestMessage MessageType = "jreq" 17 | UplinkDataFrameMessage MessageType = "updf" 18 | ProprietaryDataFrameMessage MessageType = "propdf" 19 | DownlinkMessage MessageType = "dnmsg" 20 | DownlinkTransmittedMessage MessageType = "dntxed" 21 | TimeSyncMessage MessageType = "timesync" 22 | ) 23 | 24 | type messageTypePayload struct { 25 | MessageType MessageType `json:"msgtype"` 26 | } 27 | 28 | // GetMessageType returns the message type for the given paylaod. 29 | func GetMessageType(b []byte) (MessageType, error) { 30 | var pl messageTypePayload 31 | if err := json.Unmarshal(b, &pl); err != nil { 32 | return "", errors.Wrap(err, "unmarshal message-type error") 33 | } 34 | 35 | return pl.MessageType, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/msg_type_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestGetMessageType(t *testing.T) { 10 | assert := require.New(t) 11 | 12 | jsonStr := `{"msgtype": "updf"}` 13 | 14 | typ, err := GetMessageType([]byte(jsonStr)) 15 | assert.NoError(err) 16 | assert.Equal(UplinkDataFrameMessage, typ) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/radio_meta_data.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | "github.com/brocaar/lorawan/band" 10 | "github.com/brocaar/lorawan/gps" 11 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 12 | "github.com/pkg/errors" 13 | "google.golang.org/protobuf/types/known/durationpb" 14 | "google.golang.org/protobuf/types/known/timestamppb" 15 | ) 16 | 17 | // RadioMetaData contains the radio meta-data. 18 | type RadioMetaData struct { 19 | DR int `json:"DR"` 20 | Frequency uint32 `json:"Freq"` 21 | UpInfo RadioMetaDataUpInfo `json:"upinfo"` 22 | } 23 | 24 | // RadioMetaDataUpInfo contains the radio meta-data uplink info. 25 | type RadioMetaDataUpInfo struct { 26 | RxTime float64 `json:"rxtime"` 27 | RCtx uint64 `json:"rctx"` 28 | XTime uint64 `json:"xtime"` 29 | GPSTime int64 `json:"gpstime"` 30 | RSSI float32 `json:"rssi"` 31 | SNR float32 `json:"snr"` 32 | } 33 | 34 | // SetRadioMetaDataToProto sets the given parameters to the given protobuf struct. 35 | func SetRadioMetaDataToProto(loraBand band.Band, gatewayID lorawan.EUI64, rmd RadioMetaData, pb *gw.UplinkFrame) error { 36 | // 37 | // TxInfo 38 | // 39 | dr, err := loraBand.GetDataRate(rmd.DR) 40 | if err != nil { 41 | return errors.Wrap(err, "get data-rate error") 42 | } 43 | 44 | pb.TxInfo = &gw.UplinkTxInfo{ 45 | Frequency: rmd.Frequency, 46 | } 47 | 48 | switch dr.Modulation { 49 | case band.LoRaModulation: 50 | pb.TxInfo.Modulation = &gw.Modulation{ 51 | Parameters: &gw.Modulation_Lora{ 52 | Lora: &gw.LoraModulationInfo{ 53 | Bandwidth: uint32(dr.Bandwidth) * 1000, 54 | SpreadingFactor: uint32(dr.SpreadFactor), 55 | CodeRate: gw.CodeRate_CR_4_5, 56 | PolarizationInversion: false, 57 | }, 58 | }, 59 | } 60 | case band.FSKModulation: 61 | pb.TxInfo.Modulation = &gw.Modulation{ 62 | Parameters: &gw.Modulation_Fsk{ 63 | Fsk: &gw.FskModulationInfo{ 64 | Datarate: uint32(dr.BitRate), 65 | }, 66 | }, 67 | } 68 | } 69 | 70 | // 71 | // RxInfo 72 | // 73 | pb.RxInfo = &gw.UplinkRxInfo{ 74 | GatewayId: gatewayID.String(), 75 | Rssi: int32(rmd.UpInfo.RSSI), 76 | Snr: float32(rmd.UpInfo.SNR), 77 | CrcStatus: gw.CRCStatus_CRC_OK, 78 | } 79 | 80 | if rxTime := rmd.UpInfo.RxTime; rxTime != 0 { 81 | sec, nsec := math.Modf(rmd.UpInfo.RxTime) 82 | if sec != 0 { 83 | val := time.Unix(int64(sec), int64(nsec*1e9)) 84 | pb.RxInfo.GwTime = timestamppb.New(val) 85 | } 86 | } 87 | 88 | if gpsTime := rmd.UpInfo.GPSTime; gpsTime != 0 { 89 | gpsTimeDur := time.Duration(gpsTime) * time.Microsecond 90 | gpsTimeTime := time.Time(gps.NewTimeFromTimeSinceGPSEpoch(gpsTimeDur)) 91 | 92 | pb.RxInfo.TimeSinceGpsEpoch = durationpb.New(gpsTimeDur) 93 | pb.RxInfo.GwTime = timestamppb.New(gpsTimeTime) 94 | } 95 | 96 | // Context 97 | pb.RxInfo.Context = make([]byte, 16) 98 | binary.BigEndian.PutUint64(pb.RxInfo.Context[0:8], uint64(rmd.UpInfo.RCtx)) 99 | binary.BigEndian.PutUint64(pb.RxInfo.Context[8:16], uint64(rmd.UpInfo.XTime)) 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/radio_meta_data_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | "google.golang.org/protobuf/types/known/durationpb" 9 | "google.golang.org/protobuf/types/known/timestamppb" 10 | 11 | "github.com/brocaar/lorawan" 12 | "github.com/brocaar/lorawan/band" 13 | "github.com/brocaar/lorawan/gps" 14 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 15 | ) 16 | 17 | func TestSetRadioMetaDataToProto(t *testing.T) { 18 | assert := require.New(t) 19 | 20 | timeP := timestamppb.New(time.Time(gps.NewTimeFromTimeSinceGPSEpoch(5 * time.Second))) 21 | 22 | tests := []struct { 23 | Name string 24 | In RadioMetaData 25 | Out *gw.UplinkFrame 26 | Error error 27 | }{ 28 | { 29 | Name: "LoRa", 30 | In: RadioMetaData{ 31 | DR: 5, 32 | Frequency: 868100000, 33 | UpInfo: RadioMetaDataUpInfo{ 34 | RCtx: 1, 35 | XTime: 2, 36 | RSSI: 120, 37 | SNR: 5.5, 38 | }, 39 | }, 40 | Out: &gw.UplinkFrame{ 41 | TxInfo: &gw.UplinkTxInfo{ 42 | Frequency: 868100000, 43 | Modulation: &gw.Modulation{ 44 | Parameters: &gw.Modulation_Lora{ 45 | Lora: &gw.LoraModulationInfo{ 46 | Bandwidth: 125000, 47 | SpreadingFactor: 7, 48 | CodeRate: gw.CodeRate_CR_4_5, 49 | }, 50 | }, 51 | }, 52 | }, 53 | RxInfo: &gw.UplinkRxInfo{ 54 | GatewayId: "0102030405060708", 55 | Rssi: 120, 56 | Snr: 5.5, 57 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 58 | CrcStatus: gw.CRCStatus_CRC_OK, 59 | }, 60 | }, 61 | }, 62 | { 63 | Name: "FSK", 64 | In: RadioMetaData{ 65 | DR: 7, 66 | Frequency: 868100000, 67 | UpInfo: RadioMetaDataUpInfo{ 68 | RCtx: 1, 69 | XTime: 2, 70 | RSSI: 120, 71 | }, 72 | }, 73 | Out: &gw.UplinkFrame{ 74 | TxInfo: &gw.UplinkTxInfo{ 75 | Frequency: 868100000, 76 | Modulation: &gw.Modulation{ 77 | Parameters: &gw.Modulation_Fsk{ 78 | Fsk: &gw.FskModulationInfo{ 79 | Datarate: 50000, 80 | }, 81 | }, 82 | }, 83 | }, 84 | RxInfo: &gw.UplinkRxInfo{ 85 | GatewayId: "0102030405060708", 86 | Rssi: 120, 87 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 88 | CrcStatus: gw.CRCStatus_CRC_OK, 89 | }, 90 | }, 91 | }, 92 | { 93 | Name: "LoRa with GPS time", 94 | In: RadioMetaData{ 95 | DR: 5, 96 | Frequency: 868100000, 97 | UpInfo: RadioMetaDataUpInfo{ 98 | RCtx: 1, 99 | XTime: 2, 100 | RSSI: 120, 101 | SNR: 5.5, 102 | GPSTime: int64(5 * time.Second / time.Microsecond), 103 | }, 104 | }, 105 | Out: &gw.UplinkFrame{ 106 | TxInfo: &gw.UplinkTxInfo{ 107 | Frequency: 868100000, 108 | Modulation: &gw.Modulation{ 109 | Parameters: &gw.Modulation_Lora{ 110 | Lora: &gw.LoraModulationInfo{ 111 | Bandwidth: 125000, 112 | SpreadingFactor: 7, 113 | CodeRate: gw.CodeRate_CR_4_5, 114 | }, 115 | }, 116 | }, 117 | }, 118 | RxInfo: &gw.UplinkRxInfo{ 119 | GatewayId: "0102030405060708", 120 | Rssi: 120, 121 | Snr: 5.5, 122 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 123 | TimeSinceGpsEpoch: durationpb.New(5 * time.Second), 124 | GwTime: timeP, 125 | CrcStatus: gw.CRCStatus_CRC_OK, 126 | }, 127 | }, 128 | }, 129 | } 130 | 131 | b, err := band.GetConfig(band.EU868, false, lorawan.DwellTimeNoLimit) 132 | assert.NoError(err) 133 | 134 | for _, tst := range tests { 135 | t.Run(tst.Name, func(t *testing.T) { 136 | assert := require.New(t) 137 | 138 | var uf gw.UplinkFrame 139 | err := SetRadioMetaDataToProto(b, lorawan.EUI64{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, tst.In, &uf) 140 | assert.Equal(tst.Error, err) 141 | if err != nil { 142 | return 143 | } 144 | assert.Equal(tst.Out, &uf) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/router_info.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | // RouterInfoRequest implements the router-info request. 4 | type RouterInfoRequest struct { 5 | Router EUI64 `json:"router"` 6 | } 7 | 8 | // RouterInfoResponse implements the router-info response. 9 | type RouterInfoResponse struct { 10 | Router EUI64 `json:"router"` 11 | Muxs EUI64 `json:"muxs"` 12 | URI string `json:"uri"` 13 | Error string `json:"error,omitempty"` // only in case of error 14 | } 15 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/time_sync.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | // TimeSyncRequest implements the router-info request. 4 | type TimeSyncRequest struct { 5 | MessageType MessageType `json:"msgtype"` 6 | TxTime int64 `json:"txtime"` 7 | } 8 | 9 | // TimeSyncResponse implements the router-info response. 10 | type TimeSyncResponse struct { 11 | MessageType MessageType `json:"msgtype"` 12 | TxTime int64 `json:"txtime"` 13 | GPSTime int64 `json:"gpstime"` 14 | } 15 | 16 | // TimeSyncGPSTimeTransfer implements the GPS time transfer 17 | // that is initiated by the NS. 18 | type TimeSyncGPSTimeTransfer struct { 19 | MessageType MessageType `json:"msgtype"` 20 | XTime uint64 `json:"xtime"` 21 | GPSTime int64 `json:"gpstime"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/uplink_data_frame.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/brocaar/lorawan" 10 | "github.com/brocaar/lorawan/band" 11 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 12 | ) 13 | 14 | // UplinkDataFrame implements the uplink data-frame message. 15 | type UplinkDataFrame struct { 16 | RadioMetaData 17 | 18 | MessageType MessageType `json:"msgtype"` 19 | MHDR uint8 `json:"Mhdr"` 20 | DevAddr int32 `json:"DevAddr"` 21 | FCtrl uint8 `json:"FCtrl"` 22 | FCnt uint16 `json:"FCnt"` 23 | FOpts string `json:"FOpts"` 24 | FPort int `json:"FPort"` 25 | FRMPayload string `json:"FRMPayload"` 26 | MIC int32 `json:"MIC"` 27 | } 28 | 29 | // UplinkDataFrameToProto converts the UplinkDataFrame to the protobuf struct. 30 | func UplinkDataFrameToProto(loraBand band.Band, gatewayID lorawan.EUI64, updf UplinkDataFrame) (*gw.UplinkFrame, error) { 31 | var pb gw.UplinkFrame 32 | if err := SetRadioMetaDataToProto(loraBand, gatewayID, updf.RadioMetaData, &pb); err != nil { 33 | return &pb, errors.Wrap(err, "set radio meta-data error") 34 | } 35 | 36 | // MHDR 37 | pb.PhyPayload = append(pb.PhyPayload, updf.MHDR) 38 | 39 | // devAddr 40 | devAddr := make([]byte, 4) 41 | binary.LittleEndian.PutUint32(devAddr, uint32(updf.DevAddr)) 42 | pb.PhyPayload = append(pb.PhyPayload, devAddr...) 43 | 44 | // fCtrl 45 | pb.PhyPayload = append(pb.PhyPayload, updf.FCtrl) 46 | 47 | // FCnt 48 | fCnt := make([]byte, 2) 49 | binary.LittleEndian.PutUint16(fCnt, updf.FCnt) 50 | pb.PhyPayload = append(pb.PhyPayload, fCnt...) 51 | 52 | // FOpts 53 | b, err := hex.DecodeString(updf.FOpts) 54 | if err != nil { 55 | return &pb, errors.Wrap(err, "decode FOpts error") 56 | } 57 | pb.PhyPayload = append(pb.PhyPayload, b...) 58 | 59 | // FPort 60 | if updf.FPort != -1 { 61 | pb.PhyPayload = append(pb.PhyPayload, uint8(updf.FPort)) 62 | 63 | // FRPPayload 64 | if len(updf.FRMPayload) != 0 { 65 | b, err = hex.DecodeString(updf.FRMPayload) 66 | if err != nil { 67 | return &pb, errors.Wrap(err, "decode FRMPayload error") 68 | } 69 | pb.PhyPayload = append(pb.PhyPayload, b...) 70 | } 71 | } 72 | 73 | // MIC 74 | mic := make([]byte, 4) 75 | binary.LittleEndian.PutUint32(mic, uint32(updf.MIC)) 76 | pb.PhyPayload = append(pb.PhyPayload, mic...) 77 | 78 | return &pb, nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/uplink_data_frame_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brocaar/lorawan" 7 | "github.com/brocaar/lorawan/band" 8 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestUplinkDataFrameToProto(t *testing.T) { 13 | assert := require.New(t) 14 | 15 | tests := []struct { 16 | Name string 17 | In UplinkDataFrame 18 | Out *gw.UplinkFrame 19 | Error error 20 | }{ 21 | { 22 | Name: "No FPort and FRMPayload", 23 | In: UplinkDataFrame{ 24 | RadioMetaData: RadioMetaData{ 25 | DR: 5, 26 | Frequency: 868100000, 27 | UpInfo: RadioMetaDataUpInfo{ 28 | RCtx: 1, 29 | XTime: 2, 30 | RSSI: 120, 31 | SNR: 5.5, 32 | }, 33 | }, 34 | MessageType: UplinkDataFrameMessage, 35 | MHDR: 0x40, // unconfirmed data-up 36 | DevAddr: -10, 37 | FCtrl: 0x80, // ADR 38 | FCnt: 400, 39 | FOpts: "0102", // invalid, but for the purpose of testing 40 | MIC: -20, 41 | FPort: -1, 42 | }, 43 | Out: &gw.UplinkFrame{ 44 | PhyPayload: []byte{0x40, 0xf6, 0xff, 0xff, 0x0ff, 0x80, 0x90, 0x01, 0x01, 0x02, 0xec, 0xff, 0xff, 0xff}, 45 | TxInfo: &gw.UplinkTxInfo{ 46 | Frequency: 868100000, 47 | Modulation: &gw.Modulation{ 48 | Parameters: &gw.Modulation_Lora{ 49 | Lora: &gw.LoraModulationInfo{ 50 | Bandwidth: 125000, 51 | SpreadingFactor: 7, 52 | CodeRate: gw.CodeRate_CR_4_5, 53 | }, 54 | }, 55 | }, 56 | }, 57 | RxInfo: &gw.UplinkRxInfo{ 58 | GatewayId: "0102030405060708", 59 | Rssi: 120, 60 | Snr: 5.5, 61 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 62 | CrcStatus: gw.CRCStatus_CRC_OK, 63 | }, 64 | }, 65 | }, 66 | { 67 | Name: "FPort no FRMPayload", 68 | In: UplinkDataFrame{ 69 | RadioMetaData: RadioMetaData{ 70 | DR: 5, 71 | Frequency: 868100000, 72 | UpInfo: RadioMetaDataUpInfo{ 73 | RCtx: 1, 74 | XTime: 2, 75 | RSSI: 120, 76 | SNR: 5.5, 77 | }, 78 | }, 79 | MessageType: UplinkDataFrameMessage, 80 | MHDR: 0x40, // unconfirmed data-up 81 | DevAddr: -10, 82 | FCtrl: 0x80, // ADR 83 | FCnt: 400, 84 | FOpts: "0102", // invalid, but for the purpose of testing 85 | MIC: -20, 86 | FPort: 1, 87 | }, 88 | Out: &gw.UplinkFrame{ 89 | PhyPayload: []byte{0x40, 0xf6, 0xff, 0xff, 0x0ff, 0x80, 0x90, 0x01, 0x01, 0x02, 0x01, 0xec, 0xff, 0xff, 0xff}, 90 | TxInfo: &gw.UplinkTxInfo{ 91 | Frequency: 868100000, 92 | Modulation: &gw.Modulation{ 93 | Parameters: &gw.Modulation_Lora{ 94 | Lora: &gw.LoraModulationInfo{ 95 | Bandwidth: 125000, 96 | SpreadingFactor: 7, 97 | CodeRate: gw.CodeRate_CR_4_5, 98 | }, 99 | }, 100 | }, 101 | }, 102 | RxInfo: &gw.UplinkRxInfo{ 103 | GatewayId: "0102030405060708", 104 | Rssi: 120, 105 | Snr: 5.5, 106 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 107 | CrcStatus: gw.CRCStatus_CRC_OK, 108 | }, 109 | }, 110 | }, 111 | { 112 | Name: "FPort and FRMPayload", 113 | In: UplinkDataFrame{ 114 | RadioMetaData: RadioMetaData{ 115 | DR: 5, 116 | Frequency: 868100000, 117 | UpInfo: RadioMetaDataUpInfo{ 118 | RCtx: 1, 119 | XTime: 2, 120 | RSSI: 120, 121 | SNR: 5.5, 122 | }, 123 | }, 124 | MessageType: UplinkDataFrameMessage, 125 | MHDR: 0x40, // unconfirmed data-up 126 | DevAddr: -10, 127 | FCtrl: 0x80, // ADR 128 | FCnt: 400, 129 | FOpts: "0102", // invalid, but for the purpose of testing 130 | MIC: -20, 131 | FPort: 1, 132 | FRMPayload: "04030201", 133 | }, 134 | Out: &gw.UplinkFrame{ 135 | PhyPayload: []byte{0x40, 0xf6, 0xff, 0xff, 0x0ff, 0x80, 0x90, 0x01, 0x01, 0x02, 0x01, 0x04, 0x03, 0x02, 0x01, 0xec, 0xff, 0xff, 0xff}, 136 | TxInfo: &gw.UplinkTxInfo{ 137 | Frequency: 868100000, 138 | Modulation: &gw.Modulation{ 139 | Parameters: &gw.Modulation_Lora{ 140 | Lora: &gw.LoraModulationInfo{ 141 | Bandwidth: 125000, 142 | SpreadingFactor: 7, 143 | CodeRate: gw.CodeRate_CR_4_5, 144 | }, 145 | }, 146 | }, 147 | }, 148 | RxInfo: &gw.UplinkRxInfo{ 149 | GatewayId: "0102030405060708", 150 | Rssi: 120, 151 | Snr: 5.5, 152 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 153 | CrcStatus: gw.CRCStatus_CRC_OK, 154 | }, 155 | }, 156 | }, 157 | } 158 | 159 | b, err := band.GetConfig(band.EU868, false, lorawan.DwellTimeNoLimit) 160 | assert.NoError(err) 161 | 162 | for _, tst := range tests { 163 | assert := require.New(t) 164 | 165 | uf, err := UplinkDataFrameToProto(b, lorawan.EUI64{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, tst.In) 166 | assert.Equal(tst.Error, err) 167 | if err != nil { 168 | return 169 | } 170 | assert.Equal(tst.Out, uf) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/uplink_proprietary.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/brocaar/lorawan" 7 | "github.com/brocaar/lorawan/band" 8 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // UplinkProprietaryFrame implements the uplink proprietary frame. 13 | type UplinkProprietaryFrame struct { 14 | RadioMetaData 15 | 16 | MessageType MessageType `json:"msgType"` 17 | FRMPayload string `json:"FRMPayload"` 18 | } 19 | 20 | // UplinkProprietaryFrameToProto converts the UplinkProprietaryFrame to the protobuf struct. 21 | func UplinkProprietaryFrameToProto(loraBand band.Band, gatewayID lorawan.EUI64, uppf UplinkProprietaryFrame) (*gw.UplinkFrame, error) { 22 | var pb gw.UplinkFrame 23 | if err := SetRadioMetaDataToProto(loraBand, gatewayID, uppf.RadioMetaData, &pb); err != nil { 24 | return &pb, errors.Wrap(err, "set radio meta-data error") 25 | } 26 | 27 | // FRMPayload is actually the full PHYPayload: 28 | // 29 | frmPayload, err := hex.DecodeString(uppf.FRMPayload) 30 | if err != nil { 31 | return &pb, errors.Wrap(err, "decode FRMPayload field error") 32 | } 33 | pb.PhyPayload = append(pb.PhyPayload, frmPayload...) 34 | 35 | return &pb, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/uplink_proprietary_test.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brocaar/lorawan" 7 | "github.com/brocaar/lorawan/band" 8 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestUplinkProprietaryFrameToProto(t *testing.T) { 13 | tests := []struct { 14 | Name string 15 | In UplinkProprietaryFrame 16 | Out *gw.UplinkFrame 17 | Error error 18 | }{ 19 | { 20 | Name: "proprietary", 21 | In: UplinkProprietaryFrame{ 22 | RadioMetaData: RadioMetaData{ 23 | DR: 5, 24 | Frequency: 868100000, 25 | UpInfo: RadioMetaDataUpInfo{ 26 | RCtx: 1, 27 | XTime: 2, 28 | RSSI: 120, 29 | SNR: 5.5, 30 | }, 31 | }, 32 | MessageType: ProprietaryDataFrameMessage, 33 | FRMPayload: "01020304", 34 | }, 35 | Out: &gw.UplinkFrame{ 36 | PhyPayload: []byte{0x01, 0x02, 0x03, 0x04}, 37 | TxInfo: &gw.UplinkTxInfo{ 38 | Frequency: 868100000, 39 | Modulation: &gw.Modulation{ 40 | Parameters: &gw.Modulation_Lora{ 41 | Lora: &gw.LoraModulationInfo{ 42 | Bandwidth: 125000, 43 | SpreadingFactor: 7, 44 | CodeRate: gw.CodeRate_CR_4_5, 45 | }, 46 | }, 47 | }, 48 | }, 49 | RxInfo: &gw.UplinkRxInfo{ 50 | GatewayId: "0102030405060708", 51 | Rssi: 120, 52 | Snr: 5.5, 53 | Context: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 54 | CrcStatus: gw.CRCStatus_CRC_OK, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | assert := require.New(t) 61 | 62 | b, err := band.GetConfig(band.EU868, false, lorawan.DwellTimeNoLimit) 63 | assert.NoError(err) 64 | 65 | for _, tst := range tests { 66 | assert := require.New(t) 67 | 68 | uf, err := UplinkProprietaryFrameToProto(b, lorawan.EUI64{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, tst.In) 69 | assert.Equal(tst.Error, err) 70 | if err != nil { 71 | return 72 | } 73 | assert.Equal(tst.Out, uf) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/backend/basicstation/structs/version.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | // Version implements the version message. 4 | type Version struct { 5 | MessageType MessageType `json:"msgtype"` 6 | Station string `json:"station"` 7 | Firmware string `json:"firmware"` 8 | Package string `json:"package"` 9 | Model string `json:"model"` 10 | Protocol int `json:"protocol"` 11 | // Features []string `json:"features"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/backend/concentratord/concentratord_test.go: -------------------------------------------------------------------------------- 1 | package concentratord 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/go-zeromq/zmq4" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/require" 13 | "github.com/stretchr/testify/suite" 14 | "google.golang.org/protobuf/proto" 15 | 16 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/events" 17 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 18 | "github.com/brocaar/lorawan" 19 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 20 | ) 21 | 22 | type BackendTestSuite struct { 23 | suite.Suite 24 | 25 | backend *Backend 26 | pubSock zmq4.Socket 27 | repSock zmq4.Socket 28 | } 29 | 30 | func (ts *BackendTestSuite) SetupSuite() { 31 | log.SetLevel(log.ErrorLevel) 32 | } 33 | 34 | func (ts *BackendTestSuite) SetupTest() { 35 | assert := require.New(ts.T()) 36 | 37 | tempDir, err := ioutil.TempDir("", "test") 38 | assert.NoError(err) 39 | 40 | ts.pubSock = zmq4.NewPub(context.Background()) 41 | ts.repSock = zmq4.NewRep(context.Background()) 42 | 43 | assert.NoError(ts.pubSock.Listen(fmt.Sprintf("ipc://%s/events", tempDir))) 44 | assert.NoError(ts.repSock.Listen(fmt.Sprintf("ipc://%s/commands", tempDir))) 45 | 46 | var conf config.Config 47 | conf.Backend.Concentratord.EventURL = fmt.Sprintf("ipc://%s/events", tempDir) 48 | conf.Backend.Concentratord.CommandURL = fmt.Sprintf("ipc://%s/commands", tempDir) 49 | 50 | var wg sync.WaitGroup 51 | wg.Add(1) 52 | go func() { 53 | // NewBackend expects the Gateway ID 54 | msg, err := ts.repSock.Recv() 55 | assert.NoError(err) 56 | assert.Equal("gateway_id", string(msg.Bytes())) 57 | assert.NoError(ts.repSock.Send(zmq4.NewMsg([]byte{1, 2, 3, 4, 5, 6, 7, 8}))) 58 | wg.Done() 59 | }() 60 | 61 | ts.backend, err = NewBackend(conf) 62 | assert.NoError(err) 63 | 64 | subscribeEventChan := make(chan events.Subscribe, 1) 65 | ts.backend.subscribeEventFunc = func(pl events.Subscribe) { 66 | subscribeEventChan <- pl 67 | } 68 | 69 | assert.NoError(ts.backend.Start()) 70 | wg.Wait() 71 | 72 | assert.Equal(events.Subscribe{ 73 | Subscribe: true, 74 | GatewayID: lorawan.EUI64{1, 2, 3, 4, 5, 6, 7, 8}, 75 | }, <-subscribeEventChan) 76 | } 77 | 78 | func (ts *BackendTestSuite) TearDownTest() { 79 | assert := require.New(ts.T()) 80 | assert.NoError(ts.backend.Stop()) 81 | } 82 | 83 | func (ts *BackendTestSuite) TestGatewayStats() { 84 | assert := require.New(ts.T()) 85 | gatewayStatsChan := make(chan *gw.GatewayStats, 1) 86 | ts.backend.gatewayStatsFunc = func(pl *gw.GatewayStats) { 87 | gatewayStatsChan <- pl 88 | } 89 | 90 | stats := gw.GatewayStats{ 91 | GatewayId: "0102030405060708", 92 | } 93 | b, err := proto.Marshal(&stats) 94 | assert.NoError(err) 95 | 96 | assert.NoError(ts.pubSock.SendMulti(zmq4.Msg{ 97 | Frames: [][]byte{ 98 | []byte("stats"), 99 | b, 100 | }, 101 | })) 102 | 103 | recv := <-gatewayStatsChan 104 | assert.True(proto.Equal(&stats, recv)) 105 | } 106 | 107 | func (ts *BackendTestSuite) TestUplinkFrame() { 108 | assert := require.New(ts.T()) 109 | uplinkFrameChan := make(chan *gw.UplinkFrame, 1) 110 | ts.backend.uplinkFrameFunc = func(pl *gw.UplinkFrame) { 111 | uplinkFrameChan <- pl 112 | } 113 | 114 | uf := gw.UplinkFrame{ 115 | PhyPayload: []byte{1, 2, 3, 4}, 116 | RxInfo: &gw.UplinkRxInfo{ 117 | GatewayId: "0102030405060708", 118 | }, 119 | } 120 | b, err := proto.Marshal(&uf) 121 | assert.NoError(err) 122 | 123 | assert.NoError(ts.pubSock.SendMulti(zmq4.Msg{ 124 | Frames: [][]byte{ 125 | []byte("up"), 126 | b, 127 | }, 128 | })) 129 | 130 | recv := <-uplinkFrameChan 131 | assert.True(proto.Equal(&uf, recv)) 132 | } 133 | 134 | func (ts *BackendTestSuite) TestSendDownlinkFrame() { 135 | assert := require.New(ts.T()) 136 | txAckChan := make(chan *gw.DownlinkTxAck, 1) 137 | ts.backend.downlinkTxAckFunc = func(pl *gw.DownlinkTxAck) { 138 | txAckChan <- pl 139 | } 140 | 141 | down := gw.DownlinkFrame{ 142 | GatewayId: "0102030405060708", 143 | } 144 | downB, err := proto.Marshal(&down) 145 | assert.NoError(err) 146 | 147 | ack := gw.DownlinkTxAck{ 148 | GatewayId: "0102030405060708", 149 | } 150 | ackB, err := proto.Marshal(&ack) 151 | assert.NoError(err) 152 | 153 | go func() { 154 | msg, err := ts.repSock.Recv() 155 | assert.NoError(err) 156 | assert.Equal("down", string(msg.Frames[0])) 157 | assert.Equal(downB, msg.Frames[1]) 158 | assert.NoError(ts.repSock.Send(zmq4.NewMsg(ackB))) 159 | }() 160 | 161 | assert.NoError(ts.backend.SendDownlinkFrame(&down)) 162 | 163 | recv := <-txAckChan 164 | assert.True(proto.Equal(&ack, recv)) 165 | } 166 | 167 | func (ts *BackendTestSuite) TestApplyConfiguration() { 168 | assert := require.New(ts.T()) 169 | 170 | config := gw.GatewayConfiguration{ 171 | GatewayId: "0102030405060708", 172 | Version: "config-a", 173 | } 174 | configB, err := proto.Marshal(&config) 175 | assert.NoError(err) 176 | 177 | go func() { 178 | msg, err := ts.repSock.Recv() 179 | assert.NoError(err) 180 | assert.Equal("config", string(msg.Frames[0])) 181 | assert.Equal(configB, msg.Frames[1]) 182 | assert.NoError(ts.repSock.Send(zmq4.NewMsg([]byte{}))) 183 | }() 184 | 185 | assert.NoError(ts.backend.ApplyConfiguration(&config)) 186 | } 187 | 188 | func TestBackend(t *testing.T) { 189 | suite.Run(t, new(BackendTestSuite)) 190 | } 191 | -------------------------------------------------------------------------------- /internal/backend/concentratord/metrics.go: -------------------------------------------------------------------------------- 1 | package concentratord 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | ec = promauto.NewCounterVec(prometheus.CounterOpts{ 10 | Name: "backend_concentratord_event_count", 11 | Help: "The number of received events (per type)", 12 | }, []string{"event"}) 13 | 14 | cc = promauto.NewCounterVec(prometheus.CounterOpts{ 15 | Name: "backend_concentratord_command_count", 16 | Help: "The number of received commands (per type)", 17 | }, []string{"command"}) 18 | ) 19 | 20 | func eventCounter(typ string) prometheus.Counter { 21 | return ec.With(prometheus.Labels{"event": typ}) 22 | } 23 | 24 | func commandCounter(typ string) prometheus.Counter { 25 | return cc.With(prometheus.Labels{"command": typ}) 26 | } 27 | -------------------------------------------------------------------------------- /internal/backend/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/brocaar/lorawan" 4 | 5 | // Subscribe event 6 | type Subscribe struct { 7 | // Gateway ID. 8 | GatewayID lorawan.EUI64 9 | 10 | // Subscribe (true) or unsubscribe (false) the gateway. 11 | Subscribe bool 12 | } 13 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/metrics.go: -------------------------------------------------------------------------------- 1 | package semtechudp 2 | 3 | import ( 4 | "github.com/brocaar/lorawan" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "github.com/prometheus/client_golang/prometheus/promauto" 7 | ) 8 | 9 | var ( 10 | uwc = promauto.NewCounterVec(prometheus.CounterOpts{ 11 | Name: "backend_semtechudp_udp_sent_count", 12 | Help: "The number of UDP packets sent by the backend (per packet_type).", 13 | }, []string{"packet_type"}) 14 | 15 | urc = promauto.NewCounterVec(prometheus.CounterOpts{ 16 | Name: "backend_semtechudp_udp_received_count", 17 | Help: "The number of UDP packets received by the backend (per packet_type).", 18 | }, []string{"packet_type"}) 19 | 20 | gwc = promauto.NewCounter(prometheus.CounterOpts{ 21 | Name: "backend_semtechudp_gateway_connect_count", 22 | Help: "The number of gateway connections received by the backend.", 23 | }) 24 | 25 | gwd = promauto.NewCounter(prometheus.CounterOpts{ 26 | Name: "backend_semtechudp_gateway_disconnect_count", 27 | Help: "The number of gateways that disconnected from the backend.", 28 | }) 29 | 30 | ackr = promauto.NewGaugeVec(prometheus.GaugeOpts{ 31 | Name: "backend_semtechdup_gateway_ack_rate", 32 | Help: "The percentage of upstream datagrams that were acknowledged.", 33 | }, []string{"gateway_id"}) 34 | 35 | ackrc = promauto.NewCounterVec(prometheus.CounterOpts{ 36 | Name: "backend_semtechudp_gateway_ack_rate_count", 37 | Help: "The number of ack-rates reported.", 38 | }, []string{"gateway_id"}) 39 | ) 40 | 41 | func udpWriteCounter(pt string) prometheus.Counter { 42 | return uwc.With(prometheus.Labels{"packet_type": pt}) 43 | } 44 | 45 | func udpReadCounter(pt string) prometheus.Counter { 46 | return urc.With(prometheus.Labels{"packet_type": pt}) 47 | } 48 | 49 | func connectCounter() prometheus.Counter { 50 | return gwc 51 | } 52 | 53 | func disconnectCounter() prometheus.Counter { 54 | return gwd 55 | } 56 | 57 | func ackRate(gatewayID lorawan.EUI64) prometheus.Gauge { 58 | return ackr.With(prometheus.Labels{"gateway_id": gatewayID.String()}) 59 | } 60 | 61 | func ackRateCounter(gatewayID lorawan.EUI64) prometheus.Counter { 62 | return ackrc.With(prometheus.Labels{"gateway_id": gatewayID.String()}) 63 | } 64 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/packets.go: -------------------------------------------------------------------------------- 1 | //go:generate stringer -type=PacketType 2 | 3 | package packets 4 | 5 | import ( 6 | "errors" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // PacketType defines the packet type. 13 | type PacketType byte 14 | 15 | // Available packet types 16 | const ( 17 | PushData PacketType = iota 18 | PushACK 19 | PullData 20 | PullResp 21 | PullACK 22 | TXACK 23 | ) 24 | 25 | // Protocol versions 26 | const ( 27 | ProtocolVersion1 uint8 = 0x01 28 | ProtocolVersion2 uint8 = 0x02 29 | ) 30 | 31 | // Errors 32 | var ( 33 | ErrInvalidProtocolVersion = errors.New("gateway: invalid protocol version") 34 | ) 35 | 36 | // GetPacketType returns the packet type for the given packet data. 37 | func GetPacketType(data []byte) (PacketType, error) { 38 | if len(data) < 4 { 39 | return PacketType(0), errors.New("gateway: at least 4 bytes of data are expected") 40 | } 41 | 42 | if !protocolSupported(data[0]) { 43 | return PacketType(0), ErrInvalidProtocolVersion 44 | } 45 | 46 | return PacketType(data[3]), nil 47 | } 48 | 49 | func protocolSupported(p uint8) bool { 50 | if p == ProtocolVersion1 || p == ProtocolVersion2 { 51 | return true 52 | } 53 | return false 54 | } 55 | 56 | // ExpandedTime implements time.Time but (un)marshals to and from 57 | // ISO 8601 'expanded' format. 58 | type ExpandedTime time.Time 59 | 60 | // MarshalJSON implements the json.Marshaler interface. 61 | func (t ExpandedTime) MarshalJSON() ([]byte, error) { 62 | return []byte(time.Time(t).UTC().Format(`"2006-01-02 15:04:05 MST"`)), nil 63 | } 64 | 65 | // UnmarshalJSON implements the json.Unmarshaler interface. 66 | func (t *ExpandedTime) UnmarshalJSON(data []byte) error { 67 | t2, err := time.Parse(`"2006-01-02 15:04:05 MST"`, string(data)) 68 | if err != nil { 69 | return err 70 | } 71 | *t = ExpandedTime(t2) 72 | return nil 73 | } 74 | 75 | // CompactTime implements time.Time but (un)marshals to and from 76 | // ISO 8601 'compact' format. 77 | type CompactTime time.Time 78 | 79 | // MarshalJSON implements the json.Marshaler interface. 80 | func (t CompactTime) MarshalJSON() ([]byte, error) { 81 | t2 := time.Time(t) 82 | if t2.IsZero() { 83 | return []byte("null"), nil 84 | } 85 | return []byte(t2.UTC().Format(`"` + time.RFC3339Nano + `"`)), nil 86 | } 87 | 88 | // UnmarshalJSON implements the json.Unmarshaler interface. 89 | func (t *CompactTime) UnmarshalJSON(data []byte) error { 90 | if string(data) == `""` { 91 | return nil 92 | } 93 | 94 | t2, err := time.Parse(`"`+time.RFC3339Nano+`"`, string(data)) 95 | if err != nil { 96 | return err 97 | } 98 | *t = CompactTime(t2) 99 | return nil 100 | } 101 | 102 | // DatR implements the data rate which can be either a string (LoRa identifier) 103 | // or an unsigned integer in case of FSK (bits per second). 104 | type DatR struct { 105 | LRFHSS string 106 | LoRa string 107 | FSK uint32 108 | } 109 | 110 | // MarshalJSON implements the json.Marshaler interface. 111 | func (d DatR) MarshalJSON() ([]byte, error) { 112 | if d.LoRa != "" { 113 | return []byte(`"` + d.LoRa + `"`), nil 114 | } 115 | if d.LRFHSS != "" { 116 | return []byte(`"` + d.LRFHSS + `"`), nil 117 | } 118 | return []byte(strconv.FormatUint(uint64(d.FSK), 10)), nil 119 | } 120 | 121 | // UnmarshalJSON implements the json.Unmarshaler interface. 122 | func (d *DatR) UnmarshalJSON(data []byte) error { 123 | i, err := strconv.ParseUint(string(data), 10, 32) 124 | if err != nil { 125 | // remove the trailing and leading quotes 126 | str := strings.Trim(string(data), `"`) 127 | 128 | if strings.HasPrefix(str, "SF") { 129 | d.LoRa = str 130 | } else { 131 | d.LRFHSS = str 132 | } 133 | return nil 134 | } 135 | d.FSK = uint32(i) 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/packets_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDatR(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | testTable := []struct { 15 | DatR DatR 16 | String string 17 | }{ 18 | { 19 | DatR: DatR{LoRa: "SF7BW125"}, 20 | String: `"SF7BW125"`, 21 | }, 22 | { 23 | DatR: DatR{FSK: 50000}, 24 | String: "50000", 25 | }, 26 | } 27 | 28 | for _, test := range testTable { 29 | b, err := test.DatR.MarshalJSON() 30 | assert.Nil(err) 31 | assert.Equal(test.String, string(b)) 32 | 33 | var datR DatR 34 | assert.Nil(datR.UnmarshalJSON([]byte(test.String))) 35 | assert.Equal(test.DatR, datR) 36 | } 37 | } 38 | 39 | func TestCompactTime(t *testing.T) { 40 | assert := assert.New(t) 41 | 42 | tStr := "Mon Jan 2 15:04:05 -0700 MST 2006" 43 | ts, err := time.Parse(tStr, tStr) 44 | assert.Nil(err) 45 | 46 | testTable := []struct { 47 | Time CompactTime 48 | String string 49 | }{ 50 | { 51 | Time: CompactTime(ts), 52 | String: `"2006-01-02T22:04:05Z"`, 53 | }, 54 | { 55 | Time: CompactTime(time.Time{}), 56 | String: "null", 57 | }, 58 | } 59 | 60 | for _, test := range testTable { 61 | b, err := test.Time.MarshalJSON() 62 | assert.Nil(err) 63 | assert.Equal(test.String, string(b)) 64 | 65 | str := test.String 66 | if str == "null" { 67 | str = `""` 68 | } 69 | 70 | var cp CompactTime 71 | assert.Nil(cp.UnmarshalJSON([]byte(str))) 72 | assert.True(time.Time(test.Time).Equal(time.Time(cp))) 73 | } 74 | } 75 | 76 | func TestGetPacketType(t *testing.T) { 77 | assert := assert.New(t) 78 | 79 | testTable := []struct { 80 | Bytes []byte 81 | PacketType PacketType 82 | Error error 83 | }{ 84 | { 85 | Bytes: []byte{}, 86 | Error: errors.New("gateway: at least 4 bytes of data are expected"), 87 | }, 88 | { 89 | Bytes: []byte{3, 1, 3, 4}, 90 | Error: ErrInvalidProtocolVersion, 91 | }, 92 | { 93 | Bytes: []byte{2, 1, 3, 4}, 94 | PacketType: PullACK, 95 | }, 96 | } 97 | 98 | for _, test := range testTable { 99 | pt, err := GetPacketType(test.Bytes) 100 | assert.Equal(test.Error, err) 101 | assert.Equal(test.PacketType, pt) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/packettype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=PacketType"; DO NOT EDIT. 2 | 3 | package packets 4 | 5 | import "strconv" 6 | 7 | const _PacketType_name = "PushDataPushACKPullDataPullRespPullACKTXACK" 8 | 9 | var _PacketType_index = [...]uint8{0, 8, 15, 23, 31, 38, 43} 10 | 11 | func (i PacketType) String() string { 12 | if i >= PacketType(len(_PacketType_index)-1) { 13 | return "PacketType(" + strconv.FormatInt(int64(i), 10) + ")" 14 | } 15 | return _PacketType_name[_PacketType_index[i]:_PacketType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/pull_ack.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | ) 7 | 8 | // PullACKPacket is used by the server to confirm that the network route is 9 | // open and that the server can send PULL_RESP packets at any time. 10 | type PullACKPacket struct { 11 | ProtocolVersion uint8 12 | RandomToken uint16 13 | } 14 | 15 | // MarshalBinary marshals the object in binary form. 16 | func (p PullACKPacket) MarshalBinary() ([]byte, error) { 17 | out := make([]byte, 4) 18 | out[0] = p.ProtocolVersion 19 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 20 | out[3] = byte(PullACK) 21 | return out, nil 22 | } 23 | 24 | // UnmarshalBinary decodes the object from binary form. 25 | func (p *PullACKPacket) UnmarshalBinary(data []byte) error { 26 | if len(data) != 4 { 27 | return errors.New("gateway: 4 bytes of data are expected") 28 | } 29 | if data[3] != byte(PullACK) { 30 | return errors.New("gateway: identifier mismatch (PULL_ACK expected)") 31 | } 32 | if !protocolSupported(data[0]) { 33 | return ErrInvalidProtocolVersion 34 | } 35 | p.ProtocolVersion = data[0] 36 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/pull_ack_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPullACK(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | testTable := []struct { 13 | Bytes []byte 14 | PullACKPacket PullACKPacket 15 | }{ 16 | { 17 | Bytes: []byte{2, 0, 0, 4}, 18 | PullACKPacket: PullACKPacket{ProtocolVersion: ProtocolVersion2}, 19 | }, 20 | { 21 | Bytes: []byte{2, 123, 0, 4}, 22 | PullACKPacket: PullACKPacket{ 23 | ProtocolVersion: ProtocolVersion2, 24 | RandomToken: 123, 25 | }, 26 | }, 27 | } 28 | 29 | for _, test := range testTable { 30 | b, err := test.PullACKPacket.MarshalBinary() 31 | assert.Nil(err) 32 | assert.Equal(test.Bytes, b) 33 | 34 | var p PullACKPacket 35 | assert.Nil(p.UnmarshalBinary(test.Bytes)) 36 | assert.Equal(test.PullACKPacket, p) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/pull_data.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | ) 7 | 8 | // PullDataPacket is used by the gateway to poll data from the server. 9 | type PullDataPacket struct { 10 | ProtocolVersion uint8 11 | RandomToken uint16 12 | GatewayMAC [8]byte 13 | } 14 | 15 | // MarshalBinary marshals the object in binary form. 16 | func (p PullDataPacket) MarshalBinary() ([]byte, error) { 17 | out := make([]byte, 4, 12) 18 | out[0] = p.ProtocolVersion 19 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 20 | out[3] = byte(PullData) 21 | out = append(out, p.GatewayMAC[0:len(p.GatewayMAC)]...) 22 | return out, nil 23 | } 24 | 25 | // UnmarshalBinary decodes the object from binary form. 26 | func (p *PullDataPacket) UnmarshalBinary(data []byte) error { 27 | if len(data) != 12 { 28 | return errors.New("gateway: 12 bytes of data are expected") 29 | } 30 | if data[3] != byte(PullData) { 31 | return errors.New("gateway: identifier mismatch (PULL_DATA expected)") 32 | } 33 | 34 | if !protocolSupported(data[0]) { 35 | return ErrInvalidProtocolVersion 36 | } 37 | p.ProtocolVersion = data[0] 38 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 39 | for i := 0; i < 8; i++ { 40 | p.GatewayMAC[i] = data[4+i] 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/pull_data_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPullDataTest(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | testTable := []struct { 13 | Bytes []byte 14 | PullDataPacket PullDataPacket 15 | }{ 16 | { 17 | Bytes: []byte{2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}, 18 | PullDataPacket: PullDataPacket{ProtocolVersion: ProtocolVersion2}, 19 | }, 20 | { 21 | Bytes: []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}, 22 | PullDataPacket: PullDataPacket{ 23 | ProtocolVersion: ProtocolVersion2, 24 | RandomToken: 123, 25 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 26 | }, 27 | }, 28 | } 29 | 30 | for _, test := range testTable { 31 | b, err := test.PullDataPacket.MarshalBinary() 32 | assert.Nil(err) 33 | assert.Equal(test.Bytes, b) 34 | 35 | var p PullDataPacket 36 | assert.Nil(p.UnmarshalBinary(test.Bytes)) 37 | assert.Equal(test.PullDataPacket, p) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/pull_resp.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 12 | ) 13 | 14 | // PullRespPacket is used by the server to send RF packets and associated 15 | // metadata that will have to be emitted by the gateway. 16 | type PullRespPacket struct { 17 | ProtocolVersion uint8 18 | RandomToken uint16 19 | Payload PullRespPayload 20 | } 21 | 22 | // MarshalBinary marshals the object in binary form. 23 | func (p PullRespPacket) MarshalBinary() ([]byte, error) { 24 | pb, err := json.Marshal(&p.Payload) 25 | if err != nil { 26 | return nil, err 27 | } 28 | out := make([]byte, 4, 4+len(pb)) 29 | out[0] = p.ProtocolVersion 30 | 31 | if p.ProtocolVersion != ProtocolVersion1 { 32 | // these two bytes are unused in ProtocolVersion1 33 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 34 | } 35 | out[3] = byte(PullResp) 36 | out = append(out, pb...) 37 | return out, nil 38 | } 39 | 40 | // UnmarshalBinary decodes the object from binary form. 41 | func (p *PullRespPacket) UnmarshalBinary(data []byte) error { 42 | if len(data) < 5 { 43 | return errors.New("gateway: at least 5 bytes of data are expected") 44 | } 45 | if data[3] != byte(PullResp) { 46 | return errors.New("gateway: identifier mismatch (PULL_RESP expected)") 47 | } 48 | if !protocolSupported(data[0]) { 49 | return ErrInvalidProtocolVersion 50 | } 51 | p.ProtocolVersion = data[0] 52 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 53 | return json.Unmarshal(data[4:], &p.Payload) 54 | } 55 | 56 | // PullRespPayload represents the downstream JSON data structure. 57 | type PullRespPayload struct { 58 | TXPK TXPK `json:"txpk"` 59 | } 60 | 61 | // TXPK contains a RF packet to be emitted and associated metadata. 62 | type TXPK struct { 63 | Imme bool `json:"imme"` // Send packet immediately (will ignore tmst & time) 64 | RFCh uint8 `json:"rfch"` // Concentrator "RF chain" used for TX (unsigned integer) 65 | Powe uint8 `json:"powe"` // TX output power in dBm (unsigned integer, dBm precision) 66 | Ant uint8 `json:"ant"` // Antenna number on which signal has been received 67 | Brd uint32 `json:"brd"` // Concentrator board used for RX (unsigned integer) 68 | Tmst *uint32 `json:"tmst,omitempty"` // Send packet on a certain timestamp value (will ignore time) 69 | Tmms *int64 `json:"tmms,omitempty"` // Send packet at a certain GPS time (GPS synchronization required) 70 | Freq float64 `json:"freq"` // TX central frequency in MHz (unsigned float, Hz precision) 71 | Modu string `json:"modu"` // Modulation identifier "LORA" or "FSK" 72 | DatR DatR `json:"datr"` // LoRa datarate identifier (eg. SF12BW500) || FSK datarate (unsigned, in bits per second) 73 | CodR string `json:"codr,omitempty"` // LoRa ECC coding rate identifier 74 | FDev uint16 `json:"fdev,omitempty"` // FSK frequency deviation (unsigned integer, in Hz) 75 | NCRC bool `json:"ncrc,omitempty"` // If true, disable the CRC of the physical layer (optional) 76 | IPol bool `json:"ipol"` // Lora modulation polarization inversion 77 | Prea uint16 `json:"prea,omitempty"` // RF preamble size (unsigned integer) 78 | Size uint16 `json:"size"` // RF packet payload size in bytes (unsigned integer) 79 | Data []byte `json:"data"` // Base64 encoded RF packet payload, padding optional 80 | } 81 | 82 | // GetPullRespPacket returns a PullRespPacket for the given gw.DownlinkFrame. 83 | func GetPullRespPacket(protoVersion uint8, randomToken uint16, frame *gw.DownlinkFrame, index int) (PullRespPacket, error) { 84 | if index > len(frame.Items)-1 { 85 | return PullRespPacket{}, fmt.Errorf("invalid frame index: %d", index) 86 | } 87 | 88 | item := frame.Items[index] 89 | txInfo := item.GetTxInfo() 90 | 91 | packet := PullRespPacket{ 92 | ProtocolVersion: protoVersion, 93 | RandomToken: randomToken, 94 | Payload: PullRespPayload{ 95 | TXPK: TXPK{ 96 | Freq: float64(txInfo.GetFrequency()) / 1000000, 97 | Powe: uint8(txInfo.GetPower()), 98 | Size: uint16(len(item.PhyPayload)), 99 | Data: item.PhyPayload, 100 | Ant: uint8(txInfo.GetAntenna()), 101 | Brd: uint32(txInfo.GetBoard()), 102 | }, 103 | }, 104 | } 105 | 106 | if lora := txInfo.GetModulation().GetLora(); lora != nil { 107 | packet.Payload.TXPK.Modu = "LORA" 108 | packet.Payload.TXPK.DatR.LoRa = fmt.Sprintf("SF%dBW%d", lora.SpreadingFactor, lora.Bandwidth/1000) 109 | packet.Payload.TXPK.IPol = lora.PolarizationInversion 110 | 111 | switch lora.GetCodeRate() { 112 | case gw.CodeRate_CR_4_5: 113 | packet.Payload.TXPK.CodR = "4/5" 114 | case gw.CodeRate_CR_4_6: 115 | packet.Payload.TXPK.CodR = "4/6" 116 | case gw.CodeRate_CR_4_7: 117 | packet.Payload.TXPK.CodR = "4/7" 118 | case gw.CodeRate_CR_4_8: 119 | packet.Payload.TXPK.CodR = "4/8" 120 | case gw.CodeRate_CR_LI_4_5: 121 | packet.Payload.TXPK.CodR = "4/5LI" 122 | case gw.CodeRate_CR_LI_4_6: 123 | packet.Payload.TXPK.CodR = "4/6LI" 124 | case gw.CodeRate_CR_LI_4_8: 125 | packet.Payload.TXPK.CodR = "4/8LI" 126 | default: 127 | return PullRespPacket{}, fmt.Errorf("invalid CodeRate: %s", lora.GetCodeRate()) 128 | } 129 | } 130 | 131 | if fsk := txInfo.GetModulation().GetFsk(); fsk != nil { 132 | packet.Payload.TXPK.Modu = "FSK" 133 | packet.Payload.TXPK.DatR.FSK = fsk.Datarate 134 | packet.Payload.TXPK.FDev = uint16(fsk.FrequencyDeviation) 135 | } 136 | 137 | if imm := txInfo.GetTiming().GetImmediately(); imm != nil { 138 | packet.Payload.TXPK.Imme = true 139 | } 140 | 141 | if delay := txInfo.GetTiming().GetDelay(); delay != nil { 142 | if len(txInfo.GetContext()) < 4 { 143 | return packet, fmt.Errorf("context must contain at least 4 bytes, got: %d", len(txInfo.GetContext())) 144 | } 145 | 146 | timestamp := binary.BigEndian.Uint32(txInfo.GetContext()[0:4]) 147 | timestamp += uint32(delay.GetDelay().AsDuration() / time.Microsecond) 148 | packet.Payload.TXPK.Tmst = ×tamp 149 | } 150 | 151 | if gpsEpoch := txInfo.GetTiming().GetGpsEpoch(); gpsEpoch != nil { 152 | dur := gpsEpoch.TimeSinceGpsEpoch.AsDuration() 153 | durMS := int64(dur / time.Millisecond) 154 | packet.Payload.TXPK.Tmms = &durMS 155 | } 156 | 157 | return packet, nil 158 | } 159 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/push_ack.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | ) 7 | 8 | // PushACKPacket is used by the server to acknowledge immediately all the 9 | // PUSH_DATA packets received. 10 | type PushACKPacket struct { 11 | ProtocolVersion uint8 12 | RandomToken uint16 13 | } 14 | 15 | // MarshalBinary marshals the object in binary form. 16 | func (p PushACKPacket) MarshalBinary() ([]byte, error) { 17 | out := make([]byte, 4) 18 | out[0] = p.ProtocolVersion 19 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 20 | out[3] = byte(PushACK) 21 | return out, nil 22 | } 23 | 24 | // UnmarshalBinary decodes the object from binary form. 25 | func (p *PushACKPacket) UnmarshalBinary(data []byte) error { 26 | if len(data) != 4 { 27 | return errors.New("gateway: 4 bytes of data are expected") 28 | } 29 | if data[3] != byte(PushACK) { 30 | return errors.New("gateway: identifier mismatch (PUSH_ACK expected)") 31 | } 32 | 33 | if !protocolSupported(data[0]) { 34 | return ErrInvalidProtocolVersion 35 | } 36 | p.ProtocolVersion = data[0] 37 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/push_ack_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPushACKPacket(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | testTable := []struct { 13 | Bytes []byte 14 | PushACKPacket PushACKPacket 15 | }{ 16 | { 17 | Bytes: []byte{2, 0, 0, 1}, 18 | PushACKPacket: PushACKPacket{ProtocolVersion: ProtocolVersion2}, 19 | }, 20 | { 21 | Bytes: []byte{2, 123, 0, 1}, 22 | PushACKPacket: PushACKPacket{ 23 | ProtocolVersion: ProtocolVersion2, 24 | RandomToken: 123, 25 | }, 26 | }, 27 | } 28 | 29 | for _, test := range testTable { 30 | b, err := test.PushACKPacket.MarshalBinary() 31 | assert.Nil(err) 32 | assert.Equal(test.Bytes, b) 33 | 34 | var pap PushACKPacket 35 | assert.Nil(pap.UnmarshalBinary(test.Bytes)) 36 | assert.Equal(test.PushACKPacket, pap) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/tx_ack.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "errors" 7 | 8 | "github.com/brocaar/lorawan" 9 | ) 10 | 11 | // TXACKPacket is used by the gateway to send a feedback to the server 12 | // to inform if a downlink request has been accepted or rejected by the 13 | // gateway. 14 | type TXACKPacket struct { 15 | ProtocolVersion uint8 16 | RandomToken uint16 17 | GatewayMAC lorawan.EUI64 18 | Payload *TXACKPayload 19 | } 20 | 21 | // MarshalBinary marshals the object into binary form. 22 | func (p TXACKPacket) MarshalBinary() ([]byte, error) { 23 | var pb []byte 24 | if p.Payload != nil { 25 | var err error 26 | pb, err = json.Marshal(p.Payload) 27 | if err != nil { 28 | return nil, err 29 | } 30 | } 31 | 32 | out := make([]byte, 4, len(pb)+12) 33 | out[0] = p.ProtocolVersion 34 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 35 | out[3] = byte(TXACK) 36 | out = append(out, p.GatewayMAC[:]...) 37 | out = append(out, pb...) 38 | return out, nil 39 | } 40 | 41 | // UnmarshalBinary decodes the object from binary form. 42 | func (p *TXACKPacket) UnmarshalBinary(data []byte) error { 43 | if len(data) < 12 { 44 | return errors.New("gateway: at least 12 bytes of data are expected") 45 | } 46 | if data[3] != byte(TXACK) { 47 | return errors.New("gateway: identifier mismatch (TXACK expected)") 48 | } 49 | if !protocolSupported(data[0]) { 50 | return ErrInvalidProtocolVersion 51 | } 52 | p.ProtocolVersion = data[0] 53 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 54 | for i := 0; i < 8; i++ { 55 | p.GatewayMAC[i] = data[4+i] 56 | } 57 | if len(data) > 13 { // the min payload + the length of at least "{}" 58 | p.Payload = &TXACKPayload{} 59 | return json.Unmarshal(data[12:], p.Payload) 60 | } 61 | return nil 62 | } 63 | 64 | // TXACKPayload contains the TXACKPacket payload. 65 | type TXACKPayload struct { 66 | TXPKACK TXPKACK `json:"txpk_ack"` 67 | } 68 | 69 | // TXPKACK contains the status information of the associated PULL_RESP 70 | // packet. 71 | type TXPKACK struct { 72 | Error string `json:"error"` 73 | } 74 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/packets/tx_ack_test.go: -------------------------------------------------------------------------------- 1 | package packets 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTXACK(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | testTable := []struct { 13 | Bytes []byte 14 | TXACKPacket TXACKPacket 15 | }{ 16 | { 17 | Bytes: []byte{2, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}, 18 | TXACKPacket: TXACKPacket{ProtocolVersion: ProtocolVersion2}, 19 | }, 20 | { 21 | Bytes: []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}, 22 | TXACKPacket: TXACKPacket{ 23 | ProtocolVersion: ProtocolVersion2, 24 | RandomToken: 123, 25 | GatewayMAC: [8]byte{8, 7, 6, 5, 4, 3, 2, 1}, 26 | }, 27 | }, 28 | { 29 | Bytes: []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1, 0}, 30 | TXACKPacket: TXACKPacket{ 31 | ProtocolVersion: ProtocolVersion2, 32 | RandomToken: 123, 33 | GatewayMAC: [8]byte{8, 7, 6, 5, 4, 3, 2, 1}, 34 | }, 35 | }, 36 | { 37 | Bytes: []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}, 38 | TXACKPacket: TXACKPacket{ 39 | ProtocolVersion: ProtocolVersion2, 40 | RandomToken: 123, 41 | Payload: &TXACKPayload{ 42 | TXPKACK: TXPKACK{ 43 | Error: "COLLISION_BEACON", 44 | }, 45 | }, 46 | }, 47 | }, 48 | } 49 | 50 | for _, test := range testTable { 51 | b, err := test.TXACKPacket.MarshalBinary() 52 | assert.Nil(err) 53 | 54 | // iBTS 0 byte when no payload 55 | if len(test.Bytes) == 13 { 56 | assert.Equal(test.Bytes[:12], b) 57 | } else { 58 | assert.Equal(test.Bytes, b) 59 | } 60 | 61 | var p TXACKPacket 62 | assert.Nil(p.UnmarshalBinary(test.Bytes)) 63 | assert.Equal(test.TXACKPacket, p) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/registry.go: -------------------------------------------------------------------------------- 1 | package semtechudp 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/events" 10 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/stats" 11 | "github.com/brocaar/lorawan" 12 | ) 13 | 14 | // errors 15 | var ( 16 | errGatewayDoesNotExist = errors.New("gateway does not exist") 17 | ) 18 | 19 | // gateway contains a connection and meta-data for a gateway connection. 20 | type gateway struct { 21 | stats *stats.Collector 22 | addr *net.UDPAddr 23 | lastSeen time.Time 24 | protocolVersion uint8 25 | } 26 | 27 | // gateways contains the gateways registry. 28 | type gateways struct { 29 | sync.RWMutex 30 | gateways map[lorawan.EUI64]gateway 31 | cleanupDuration time.Duration 32 | 33 | subscribeEventFunc func(events.Subscribe) 34 | } 35 | 36 | // get returns the gateway object for the given MAC. 37 | func (c *gateways) get(mac lorawan.EUI64) (gateway, error) { 38 | c.RLock() 39 | defer c.RUnlock() 40 | 41 | gw, ok := c.gateways[mac] 42 | if !ok { 43 | return gw, errGatewayDoesNotExist 44 | } 45 | 46 | return gw, nil 47 | } 48 | 49 | // Set creates or updates the gateway for the given Gateway ID. 50 | // Note that set must only be called for PullData frames! The UDP Packet 51 | // Forwarded uses two UDP sockets and the socket responsible for sending the 52 | // PullData is used for receiving downlink data. 53 | func (c *gateways) set(gatewayID lorawan.EUI64, gw gateway) error { 54 | c.Lock() 55 | defer c.Unlock() 56 | 57 | gww, ok := c.gateways[gatewayID] 58 | if !ok { 59 | gw.stats = stats.NewCollector() 60 | connectCounter().Inc() 61 | } else { 62 | gw.stats = gww.stats 63 | } 64 | 65 | if c.subscribeEventFunc != nil { 66 | c.subscribeEventFunc(events.Subscribe{ 67 | Subscribe: true, 68 | GatewayID: gatewayID, 69 | }) 70 | } 71 | 72 | c.gateways[gatewayID] = gw 73 | return nil 74 | } 75 | 76 | // cleanup removes inactive gateways from the registry. 77 | func (c *gateways) cleanup() error { 78 | c.Lock() 79 | defer c.Unlock() 80 | 81 | for gatewayID := range c.gateways { 82 | if c.gateways[gatewayID].lastSeen.Before(time.Now().Add(c.cleanupDuration)) { 83 | disconnectCounter().Inc() 84 | 85 | if c.subscribeEventFunc != nil { 86 | c.subscribeEventFunc(events.Subscribe{ 87 | Subscribe: false, 88 | GatewayID: gatewayID, 89 | }) 90 | } 91 | 92 | delete(c.gateways, gatewayID) 93 | } 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /internal/backend/semtechudp/test/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "SX1301_conf": { 3 | "lorawan_public": true, 4 | "clksrc": 1, /* radio_1 provides clock to concentrator */ 5 | "antenna_gain": 0, /* antenna gain, in dBi */ 6 | "radio_0": { 7 | "enable": true, 8 | "type": "SX1257", 9 | "freq": 867500000, 10 | "rssi_offset": -166.0, 11 | "tx_enable": true, 12 | "tx_freq_min": 863000000, 13 | "tx_freq_max": 870000000 14 | }, 15 | "radio_1": { 16 | "enable": true, 17 | "type": "SX1257", 18 | "freq": 868500000, 19 | "rssi_offset": -166.0, 20 | "tx_enable": false 21 | }, 22 | "chan_multiSF_0": { 23 | /* Lora MAC channel, 125kHz, all SF, 868.1 MHz */ 24 | "enable": true, 25 | "radio": 1, 26 | "if": -400000 27 | }, 28 | "chan_multiSF_1": { 29 | /* Lora MAC channel, 125kHz, all SF, 868.3 MHz */ 30 | "enable": true, 31 | "radio": 1, 32 | "if": -200000 33 | }, 34 | "chan_multiSF_2": { 35 | /* Lora MAC channel, 125kHz, all SF, 868.5 MHz */ 36 | "enable": true, 37 | "radio": 1, 38 | "if": 0 39 | }, 40 | "chan_multiSF_3": { 41 | /* Lora MAC channel, 125kHz, all SF, 867.1 MHz */ 42 | "enable": true, 43 | "radio": 0, 44 | "if": -400000 45 | }, 46 | "chan_multiSF_4": { 47 | /* Lora MAC channel, 125kHz, all SF, 867.3 MHz */ 48 | "enable": true, 49 | "radio": 0, 50 | "if": -200000 51 | }, 52 | "chan_multiSF_5": { 53 | /* Lora MAC channel, 125kHz, all SF, 867.5 MHz */ 54 | "enable": true, 55 | "radio": 0, 56 | "if": 0 57 | }, 58 | "chan_multiSF_6": { 59 | /* Lora MAC channel, 125kHz, all SF, 867.7 MHz */ 60 | "enable": true, 61 | "radio": 0, 62 | "if": 200000 63 | }, 64 | "chan_multiSF_7": { 65 | /* Lora MAC channel, 125kHz, all SF, 867.9 MHz */ 66 | "enable": true, 67 | "radio": 0, 68 | "if": 400000 69 | }, 70 | "chan_Lora_std": { 71 | /* Lora MAC channel, 250kHz, SF7, 868.3 MHz */ 72 | "enable": true, 73 | "radio": 1, 74 | "if": -200000, 75 | "bandwidth": 250000, 76 | "spread_factor": 7 77 | }, 78 | "chan_FSK": { 79 | /* FSK 50kbps channel, 868.8 MHz */ 80 | "enable": true, 81 | "radio": 1, 82 | "if": 300000, 83 | "bandwidth": 125000, 84 | "datarate": 50000 85 | }, 86 | "tx_lut_0": { 87 | /* TX gain table, index 0 */ 88 | "pa_gain": 0, 89 | "mix_gain": 8, 90 | "rf_power": -6, 91 | "dig_gain": 0 92 | }, 93 | "tx_lut_1": { 94 | /* TX gain table, index 1 */ 95 | "pa_gain": 0, 96 | "mix_gain": 10, 97 | "rf_power": -3, 98 | "dig_gain": 0 99 | }, 100 | "tx_lut_2": { 101 | /* TX gain table, index 2 */ 102 | "pa_gain": 0, 103 | "mix_gain": 12, 104 | "rf_power": 0, 105 | "dig_gain": 0 106 | }, 107 | "tx_lut_3": { 108 | /* TX gain table, index 3 */ 109 | "pa_gain": 1, 110 | "mix_gain": 8, 111 | "rf_power": 3, 112 | "dig_gain": 0 113 | }, 114 | "tx_lut_4": { 115 | /* TX gain table, index 4 */ 116 | "pa_gain": 1, 117 | "mix_gain": 10, 118 | "rf_power": 6, 119 | "dig_gain": 0 120 | }, 121 | "tx_lut_5": { 122 | /* TX gain table, index 5 */ 123 | "pa_gain": 1, 124 | "mix_gain": 12, 125 | "rf_power": 10, 126 | "dig_gain": 0 127 | }, 128 | "tx_lut_6": { 129 | /* TX gain table, index 6 */ 130 | "pa_gain": 1, 131 | "mix_gain": 13, 132 | "rf_power": 11, 133 | "dig_gain": 0 134 | }, 135 | "tx_lut_7": { 136 | /* TX gain table, index 7 */ 137 | "pa_gain": 2, 138 | "mix_gain": 9, 139 | "rf_power": 12, 140 | "dig_gain": 0 141 | }, 142 | "tx_lut_8": { 143 | /* TX gain table, index 8 */ 144 | "pa_gain": 1, 145 | "mix_gain": 15, 146 | "rf_power": 13, 147 | "dig_gain": 0 148 | }, 149 | "tx_lut_9": { 150 | /* TX gain table, index 9 */ 151 | "pa_gain": 2, 152 | "mix_gain": 10, 153 | "rf_power": 14, 154 | "dig_gain": 0 155 | }, 156 | "tx_lut_10": { 157 | /* TX gain table, index 10 */ 158 | "pa_gain": 2, 159 | "mix_gain": 11, 160 | "rf_power": 16, 161 | "dig_gain": 0 162 | }, 163 | "tx_lut_11": { 164 | /* TX gain table, index 11 */ 165 | "pa_gain": 3, 166 | "mix_gain": 9, 167 | "rf_power": 20, 168 | "dig_gain": 0 169 | } 170 | }, 171 | 172 | "gateway_conf": { 173 | "gateway_ID": "AA555A0000000000", 174 | /* change with default server address/ports, or overwrite in local_conf.json */ 175 | "server_address": "localhost", 176 | "serv_port_up": 1680, 177 | "serv_port_down": 1680, 178 | /* adjust the following parameters for your network */ 179 | "keepalive_interval": 10, 180 | "stat_interval": 30, 181 | "push_timeout_ms": 100, 182 | /* forward only valid packets */ 183 | "forward_crc_valid": true, 184 | "forward_crc_error": false, 185 | "forward_crc_disabled": false 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /internal/backend/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "encoding/hex" 5 | "sync" 6 | 7 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | type Collector struct { 12 | sync.Mutex 13 | 14 | rxCount uint32 15 | txCount uint32 16 | 17 | rxPerFreqCount map[uint32]uint32 18 | txPerFreqCount map[uint32]uint32 19 | 20 | rxPerModulationCount map[string]uint32 21 | txPerModulationCount map[string]uint32 22 | 23 | txStatusCount map[string]uint32 24 | } 25 | 26 | func NewCollector() *Collector { 27 | c := Collector{} 28 | c.reset() 29 | return &c 30 | } 31 | 32 | func (c *Collector) CountUplink(uf *gw.UplinkFrame) { 33 | c.Lock() 34 | defer c.Unlock() 35 | 36 | mod := uf.GetTxInfo().GetModulation() 37 | 38 | b, err := proto.Marshal(mod) 39 | if err != nil { 40 | return 41 | } 42 | modStr := hex.EncodeToString(b) 43 | 44 | c.rxCount = c.rxCount + 1 45 | c.rxPerFreqCount[uf.GetTxInfo().Frequency] = c.rxPerFreqCount[uf.GetTxInfo().Frequency] + 1 46 | c.rxPerModulationCount[modStr] = c.rxPerModulationCount[modStr] + 1 47 | } 48 | 49 | func (c *Collector) CountDownlink(dl *gw.DownlinkFrame, ack *gw.DownlinkTxAck) { 50 | c.Lock() 51 | defer c.Unlock() 52 | 53 | for i, item := range ack.Items { 54 | if item.Status == gw.TxAckStatus_IGNORED { 55 | continue 56 | } 57 | 58 | status := item.Status.String() 59 | c.txStatusCount[status] = c.txStatusCount[status] + 1 60 | 61 | if item.Status == gw.TxAckStatus_OK && i < len(dl.Items) { 62 | mod := dl.Items[i].GetTxInfo().GetModulation() 63 | 64 | b, err := proto.Marshal(mod) 65 | if err != nil { 66 | return 67 | } 68 | modStr := hex.EncodeToString(b) 69 | 70 | c.txCount = c.txCount + 1 71 | c.txPerFreqCount[dl.Items[i].GetTxInfo().Frequency] = c.txPerFreqCount[dl.Items[i].GetTxInfo().Frequency] + 1 72 | c.txPerModulationCount[modStr] = c.txPerModulationCount[modStr] + 1 73 | } 74 | } 75 | 76 | } 77 | 78 | func (c *Collector) ExportStats() *gw.GatewayStats { 79 | c.Lock() 80 | defer c.Unlock() 81 | 82 | stats := gw.GatewayStats{ 83 | RxPacketsReceived: c.rxCount, 84 | RxPacketsReceivedOk: c.rxCount, 85 | TxPacketsReceived: c.txCount, 86 | TxPacketsEmitted: c.txCount, 87 | RxPacketsPerFrequency: make(map[uint32]uint32), 88 | TxPacketsPerFrequency: make(map[uint32]uint32), 89 | RxPacketsPerModulation: make([]*gw.PerModulationCount, 0), 90 | TxPacketsPerModulation: make([]*gw.PerModulationCount, 0), 91 | TxPacketsPerStatus: make(map[string]uint32), 92 | } 93 | 94 | for f, c := range c.rxPerFreqCount { 95 | stats.RxPacketsPerFrequency[f] = c 96 | } 97 | 98 | for f, c := range c.txPerFreqCount { 99 | stats.TxPacketsPerFrequency[f] = c 100 | } 101 | 102 | for bStr, c := range c.rxPerModulationCount { 103 | b, _ := hex.DecodeString(bStr) 104 | var mod gw.Modulation 105 | _ = proto.Unmarshal(b, &mod) 106 | 107 | stats.RxPacketsPerModulation = append(stats.RxPacketsPerModulation, &gw.PerModulationCount{ 108 | Count: c, 109 | Modulation: &mod, 110 | }) 111 | } 112 | 113 | for bStr, c := range c.txPerModulationCount { 114 | b, _ := hex.DecodeString(bStr) 115 | var mod gw.Modulation 116 | _ = proto.Unmarshal(b, &mod) 117 | 118 | stats.TxPacketsPerModulation = append(stats.TxPacketsPerModulation, &gw.PerModulationCount{ 119 | Count: c, 120 | Modulation: &mod, 121 | }) 122 | } 123 | 124 | for s, c := range c.txStatusCount { 125 | stats.TxPacketsPerStatus[s] = c 126 | } 127 | 128 | c.reset() 129 | return &stats 130 | } 131 | 132 | func (c *Collector) reset() { 133 | c.rxCount = 0 134 | c.rxCount = 0 135 | c.txCount = 0 136 | c.rxPerFreqCount = make(map[uint32]uint32) 137 | c.txPerFreqCount = make(map[uint32]uint32) 138 | c.rxPerModulationCount = make(map[string]uint32) 139 | c.txPerModulationCount = make(map[string]uint32) 140 | c.txStatusCount = make(map[string]uint32) 141 | } 142 | -------------------------------------------------------------------------------- /internal/commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "sync" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | log "github.com/sirupsen/logrus" 14 | 15 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 16 | "github.com/brocaar/chirpstack-gateway-bridge/internal/integration" 17 | "github.com/brocaar/lorawan" 18 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 19 | ) 20 | 21 | type command struct { 22 | Command string 23 | MaxExecutionDuration time.Duration 24 | } 25 | 26 | var ( 27 | mux sync.RWMutex 28 | 29 | commands map[string]command 30 | ) 31 | 32 | // Setup configures the gateway commands. 33 | func Setup(conf config.Config) error { 34 | mux.Lock() 35 | defer mux.Unlock() 36 | 37 | commands = make(map[string]command) 38 | 39 | for k, v := range conf.Commands.Commands { 40 | commands[k] = command{ 41 | Command: v.Command, 42 | MaxExecutionDuration: v.MaxExecutionDuration, 43 | } 44 | 45 | log.WithFields(log.Fields{ 46 | "command": k, 47 | "command_exec": v.Command, 48 | "max_execution_duration": v.MaxExecutionDuration, 49 | }).Info("commands: configuring command") 50 | } 51 | 52 | i := integration.GetIntegration() 53 | if i == nil { 54 | return errors.New("integration is not set") 55 | } 56 | 57 | i.SetGatewayCommandExecRequestFunc(gatewayCommandExecRequestFunc) 58 | 59 | return nil 60 | } 61 | 62 | func gatewayCommandExecRequestFunc(pl *gw.GatewayCommandExecRequest) { 63 | go executeCommand(pl) 64 | } 65 | 66 | func executeCommand(cmd *gw.GatewayCommandExecRequest) { 67 | var gatewayID lorawan.EUI64 68 | copy(gatewayID[:], cmd.GatewayId) 69 | 70 | stdout, stderr, err := execute(cmd.Command, cmd.Stdin, cmd.Environment) 71 | resp := gw.GatewayCommandExecResponse{ 72 | GatewayId: cmd.GetGatewayId(), 73 | ExecId: cmd.GetExecId(), 74 | Stdout: stdout, 75 | Stderr: stderr, 76 | } 77 | if err != nil { 78 | resp.Error = err.Error() 79 | } 80 | 81 | if err := integration.GetIntegration().PublishEvent(gatewayID, "exec", cmd.GetExecId(), &resp); err != nil { 82 | log.WithError(err).Error("commands: publish command execution event error") 83 | } 84 | } 85 | 86 | func execute(command string, stdin []byte, environment map[string]string) ([]byte, []byte, error) { 87 | mux.RLock() 88 | defer mux.RUnlock() 89 | 90 | cmd, ok := commands[command] 91 | if !ok { 92 | return nil, nil, errors.New("command does not exist") 93 | } 94 | 95 | cmdArgs, err := ParseCommandLine(cmd.Command) 96 | if err != nil { 97 | return nil, nil, errors.Wrap(err, "parse command error") 98 | } 99 | if len(cmdArgs) == 0 { 100 | return nil, nil, errors.New("no command is given") 101 | } 102 | 103 | log.WithFields(log.Fields{ 104 | "command": command, 105 | "exec": cmdArgs[0], 106 | "args": cmdArgs[1:], 107 | "max_execution_duration": cmd.MaxExecutionDuration, 108 | }).Info("commands: executing command") 109 | 110 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(cmd.MaxExecutionDuration)) 111 | defer cancel() 112 | 113 | cmdCtx := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) 114 | 115 | // The default is that when cmdCtx.Env is nil, os.Environ() are being used 116 | // automatically. As we want to add additional env. variables, we want to 117 | // extend this list, thus first need to set them to os.Environ() 118 | cmdCtx.Env = os.Environ() 119 | for k, v := range environment { 120 | cmdCtx.Env = append(cmdCtx.Env, fmt.Sprintf("%s=%s", k, v)) 121 | } 122 | 123 | stdinPipe, err := cmdCtx.StdinPipe() 124 | if err != nil { 125 | return nil, nil, errors.Wrap(err, "get stdin pipe error") 126 | } 127 | 128 | stdoutPipe, err := cmdCtx.StdoutPipe() 129 | if err != nil { 130 | return nil, nil, errors.Wrap(err, "get stdout pipe error") 131 | } 132 | 133 | stderrPipe, err := cmdCtx.StderrPipe() 134 | if err != nil { 135 | return nil, nil, errors.Wrap(err, "get stderr pipe error") 136 | } 137 | 138 | go func() { 139 | defer stdinPipe.Close() 140 | if _, err := stdinPipe.Write(stdin); err != nil { 141 | log.WithError(err).Error("commands: write to stdin error") 142 | } 143 | }() 144 | 145 | if err := cmdCtx.Start(); err != nil { 146 | return nil, nil, errors.Wrap(err, "starting command error") 147 | } 148 | 149 | stdoutB, _ := ioutil.ReadAll(stdoutPipe) 150 | stderrB, _ := ioutil.ReadAll(stderrPipe) 151 | 152 | if err := cmdCtx.Wait(); err != nil { 153 | return nil, nil, errors.Wrap(err, "waiting for command to finish error") 154 | } 155 | 156 | return stdoutB, stderrB, nil 157 | } 158 | 159 | // ParseCommandLine parses the given command to commands and arguments. 160 | // source: https://stackoverflow.com/questions/34118732/parse-a-command-line-string-into-flags-and-arguments-in-golang 161 | func ParseCommandLine(command string) ([]string, error) { 162 | var args []string 163 | state := "start" 164 | current := "" 165 | quote := "\"" 166 | escapeNext := true 167 | for i := 0; i < len(command); i++ { 168 | c := command[i] 169 | 170 | if state == "quotes" { 171 | if string(c) != quote { 172 | current += string(c) 173 | } else { 174 | args = append(args, current) 175 | current = "" 176 | state = "start" 177 | } 178 | continue 179 | } 180 | 181 | if escapeNext { 182 | current += string(c) 183 | escapeNext = false 184 | continue 185 | } 186 | 187 | if c == '\\' { 188 | escapeNext = true 189 | continue 190 | } 191 | 192 | if c == '"' || c == '\'' { 193 | state = "quotes" 194 | quote = string(c) 195 | continue 196 | } 197 | 198 | if state == "arg" { 199 | if c == ' ' || c == '\t' { 200 | args = append(args, current) 201 | current = "" 202 | state = "start" 203 | } else { 204 | current += string(c) 205 | } 206 | continue 207 | } 208 | 209 | if c != ' ' && c != '\t' { 210 | state = "arg" 211 | current += string(c) 212 | } 213 | } 214 | 215 | if state == "quotes" { 216 | return []string{}, errors.New(fmt.Sprintf("Unclosed quote in command line: %s", command)) 217 | } 218 | 219 | if current != "" { 220 | args = append(args, current) 221 | } 222 | 223 | return args, nil 224 | } 225 | -------------------------------------------------------------------------------- /internal/commands/commands_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParseCommandLine(t *testing.T) { 12 | assert := require.New(t) 13 | 14 | tests := []struct { 15 | In string 16 | Out []string 17 | Error error 18 | }{ 19 | { 20 | In: "/path/to/bin arg1 arg2 arg3", 21 | Out: []string{"/path/to/bin", "arg1", "arg2", "arg3"}, 22 | }, 23 | } 24 | 25 | for _, tst := range tests { 26 | out, err := ParseCommandLine(tst.In) 27 | assert.Equal(tst.Error, err) 28 | if err != nil { 29 | continue 30 | } 31 | assert.Equal(tst.Out, out) 32 | } 33 | } 34 | 35 | func TestExecute(t *testing.T) { 36 | tests := []struct { 37 | Name string 38 | Commands map[string]command 39 | 40 | Command string 41 | Stdin []byte 42 | Environment map[string]string 43 | 44 | ExpectedStdout []byte 45 | ExpectedStdErr []byte 46 | ExpectedError error 47 | }{ 48 | { 49 | Name: "command not configured", 50 | Command: "reboot", 51 | ExpectedError: errors.New("command does not exist"), 52 | }, 53 | { 54 | Name: "word count stdin", 55 | Commands: map[string]command{ 56 | "wordcount": command{ 57 | Command: "wc -w", 58 | MaxExecutionDuration: time.Second, 59 | }, 60 | }, 61 | Command: "wordcount", 62 | Stdin: []byte("foo bar test bar"), 63 | ExpectedStdout: []byte("4\n"), 64 | ExpectedStdErr: []byte{}, 65 | }, 66 | { 67 | Name: "execution time epxired", 68 | Commands: map[string]command{ 69 | "sleep": command{ 70 | Command: "sleep 1", 71 | MaxExecutionDuration: time.Millisecond, 72 | }, 73 | }, 74 | Command: "sleep", 75 | ExpectedError: errors.New("waiting for command to finish error: signal: killed"), 76 | }, 77 | { 78 | Name: "environment variables", 79 | Commands: map[string]command{ 80 | "printenv": command{ 81 | Command: "printenv FOO", 82 | MaxExecutionDuration: time.Second, 83 | }, 84 | }, 85 | Command: "printenv", 86 | Environment: map[string]string{ 87 | "FOO": "bar", 88 | }, 89 | ExpectedStdout: []byte("bar\n"), 90 | ExpectedStdErr: []byte{}, 91 | }, 92 | { 93 | Name: "stdout and stderr", 94 | Commands: map[string]command{ 95 | "echo": command{ 96 | Command: `sh -c 'echo "foo" >&1; echo "bar" >&2'`, 97 | MaxExecutionDuration: time.Second, 98 | }, 99 | }, 100 | Command: "echo", 101 | ExpectedStdout: []byte("foo\n"), 102 | ExpectedStdErr: []byte("bar\n"), 103 | }, 104 | { 105 | Name: "executable not found", 106 | Commands: map[string]command{ 107 | "foobar": command{ 108 | Command: "foobartest", 109 | MaxExecutionDuration: time.Second, 110 | }, 111 | }, 112 | Command: "foobar", 113 | ExpectedError: errors.New(`starting command error: exec: "foobartest": executable file not found in $PATH`), 114 | }, 115 | } 116 | 117 | for _, tst := range tests { 118 | t.Run(tst.Name, func(t *testing.T) { 119 | assert := require.New(t) 120 | 121 | commands = tst.Commands 122 | 123 | stdout, stderr, err := execute(tst.Command, tst.Stdin, tst.Environment) 124 | if tst.ExpectedError != nil && err != nil { 125 | assert.Equal(tst.ExpectedError.Error(), err.Error()) 126 | } else { 127 | assert.Equal(tst.ExpectedError, err) 128 | } 129 | assert.Equal(tst.ExpectedStdout, stdout) 130 | assert.Equal(tst.ExpectedStdErr, stderr) 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Config defines the configuration structure. 8 | type Config struct { 9 | General struct { 10 | LogJSON bool `mapstructure:"log_json"` 11 | LogLevel int `mapstructure:"log_level"` 12 | LogToSyslog bool `mapstructure:"log_to_syslog"` 13 | } `mapstructure:"general"` 14 | 15 | Filters struct { 16 | NetIDs []string `mapstructure:"net_ids"` 17 | JoinEUIs [][2]string `mapstructure:"join_euis"` 18 | } `mapstructure:"filters"` 19 | 20 | Backend struct { 21 | Type string `mapstructure:"type"` 22 | 23 | SemtechUDP struct { 24 | UDPBind string `mapstructure:"udp_bind"` 25 | SkipCRCCheck bool `mapstructure:"skip_crc_check"` 26 | FakeRxTime bool `mapstructure:"fake_rx_time"` 27 | CleanupDuration int `mapstructure:"connection_timeout_duration"` 28 | } `mapstructure:"semtech_udp"` 29 | 30 | BasicStation struct { 31 | Bind string `mapstructure:"bind"` 32 | TLSSupportProxy bool `mapstructure:"tls_support_proxy"` 33 | TLSCert string `mapstructure:"tls_cert"` 34 | TLSKey string `mapstructure:"tls_key"` 35 | CACert string `mapstructure:"ca_cert"` 36 | StatsInterval time.Duration `mapstructure:"stats_interval"` 37 | PingInterval time.Duration `mapstructure:"ping_interval"` 38 | TimesyncInterval time.Duration `mapstructure:"timesync_interval"` 39 | ReadTimeout time.Duration `mapstructure:"read_timeout"` 40 | WriteTimeout time.Duration `mapstructure:"write_timeout"` 41 | Region string `mapstructure:"region"` 42 | FrequencyMin uint32 `mapstructure:"frequency_min"` 43 | FrequencyMax uint32 `mapstructure:"frequency_max"` 44 | Concentrators []BasicStationConcentrator `mapstructure:"concentrators"` 45 | } `mapstructure:"basic_station"` 46 | 47 | Concentratord struct { 48 | EventURL string `mapstructure:"event_url"` 49 | CommandURL string `mapstructure:"command_url"` 50 | } `mapstructure:"concentratord"` 51 | } `mapstructure:"backend"` 52 | 53 | Integration struct { 54 | Marshaler string `mapstructure:"marshaler"` 55 | 56 | MQTT struct { 57 | EventTopicTemplate string `mapstructure:"event_topic_template"` 58 | CommandTopicTemplate string `mapstructure:"command_topic_template"` 59 | StateTopicTemplate string `mapstructure:"state_topic_template"` 60 | StateRetained bool `mapstructure:"state_retained"` 61 | KeepAlive time.Duration `mapstructure:"keep_alive"` 62 | MaxReconnectInterval time.Duration `mapstructure:"max_reconnect_interval"` 63 | TerminateOnConnectError bool `mapstructure:"terminate_on_connect_error"` 64 | MaxTokenWait time.Duration `mapstructure:"max_token_wait"` 65 | 66 | Auth struct { 67 | Type string `mapstructure:"type"` 68 | 69 | Generic struct { 70 | Server string `mapstructure:"server"` 71 | Servers []string `mapstructure:"servers"` 72 | Username string `mapstructure:"username"` 73 | Password string `mapstrucure:"password"` 74 | CACert string `mapstructure:"ca_cert"` 75 | TLSCert string `mapstructure:"tls_cert"` 76 | TLSKey string `mapstructure:"tls_key"` 77 | QOS uint8 `mapstructure:"qos"` 78 | CleanSession bool `mapstructure:"clean_session"` 79 | ClientID string `mapstructure:"client_id"` 80 | } `mapstructure:"generic"` 81 | 82 | GCPCloudIoTCore struct { 83 | Server string `mapstructure:"server"` 84 | DeviceID string `mapstructure:"device_id"` 85 | ProjectID string `mapstructure:"project_id"` 86 | CloudRegion string `mapstructure:"cloud_region"` 87 | RegistryID string `mapstructure:"registry_id"` 88 | JWTExpiration time.Duration `mapstructure:"jwt_expiration"` 89 | JWTKeyFile string `mapstructure:"jwt_key_file"` 90 | } `mapstructure:"gcp_cloud_iot_core"` 91 | 92 | AzureIoTHub struct { 93 | DeviceConnectionString string `mapstructure:"device_connection_string"` 94 | DeviceID string `mapstructure:"device_id"` 95 | Hostname string `mapstructure:"hostname"` 96 | DeviceKey string `mapstructure:"-"` 97 | SASTokenExpiration time.Duration `mapstructure:"sas_token_expiration"` 98 | TLSCert string `mapstructure:"tls_cert"` 99 | TLSKey string `mapstructure:"tls_key"` 100 | } `mapstructure:"azure_iot_hub"` 101 | } `mapstructure:"auth"` 102 | } `mapstructure:"mqtt"` 103 | } `mapstructure:"integration"` 104 | 105 | Metrics struct { 106 | Prometheus struct { 107 | EndpointEnabled bool `mapstructure:"endpoint_enabled"` 108 | Bind string `mapstructure:"bind"` 109 | } `mapstructure:"prometheus"` 110 | } `mapstructure:"metrics"` 111 | 112 | MetaData struct { 113 | Static map[string]string `mapstructure:"static"` 114 | Dynamic struct { 115 | ExecutionInterval time.Duration `mapstructure:"execution_interval"` 116 | MaxExecutionDuration time.Duration `mapstructure:"max_execution_duration"` 117 | SplitDelimiter string `mapstructure:"split_delimiter"` 118 | Commands map[string]string `mapstructure:"commands"` 119 | } `mapstructure:"dynamic"` 120 | } `mapstructure:"meta_data"` 121 | 122 | Commands struct { 123 | Commands map[string]struct { 124 | MaxExecutionDuration time.Duration `mapstructure:"max_execution_duration"` 125 | Command string `mapstructure:"command"` 126 | } `mapstructure:"commands"` 127 | } `mapstructure:"commands"` 128 | } 129 | 130 | // BasicStationConcentrator holds the configuration for a BasicStation concentrator. 131 | type BasicStationConcentrator struct { 132 | MultiSF BasicStationConcentratorMultiSF `mapstructure:"multi_sf"` 133 | LoRaSTD BasicStationConcentratorLoRaSTD `mapstructure:"lora_std"` 134 | FSK BasicStationConcentratorFSK `mapstructure:"fsk"` 135 | } 136 | 137 | // BasicStationConcentratorMultiSF holds the multi-SF channels. 138 | type BasicStationConcentratorMultiSF struct { 139 | Frequencies []uint32 `mapstructure:"frequencies"` 140 | } 141 | 142 | // BasicStationConcentratorLoRaSTD holds the LoRa STD config. 143 | type BasicStationConcentratorLoRaSTD struct { 144 | Frequency uint32 `mapstructure:"frequency"` 145 | Bandwidth uint32 `mapstrcuture:"bandwidth"` 146 | SpreadingFactor uint32 `mapstructure:"spreading_factor"` 147 | } 148 | 149 | // BasicStationConcentratorFSK holds the FSK config. 150 | type BasicStationConcentratorFSK struct { 151 | Frequency uint32 `mapstructure:"frequency"` 152 | } 153 | 154 | // C holds the global configuration. 155 | var C Config 156 | -------------------------------------------------------------------------------- /internal/config/sx1301v1/sx1301v1.go: -------------------------------------------------------------------------------- 1 | // Package sx1301v1 contains helpers for generating configuration for Semtech SX1301v1 gateways. 2 | package sx1301v1 3 | 4 | import ( 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 9 | ) 10 | 11 | // radioBandwidthPerChannelBandwidth defines the bandwidth that a single radio 12 | // can cover per channel bandwidth 13 | var radioBandwidthPerChannelBandwidth = map[uint32]uint32{ 14 | 500000: 1100000, // 500kHz channel 15 | 250000: 1000000, // 250kHz channel 16 | 125000: 925000, // 125kHz channel 17 | } 18 | 19 | // defaultRadioBandwidth defines the radio bandwidth in case the channel 20 | // bandwidth does not match any of the above values. 21 | const defaultRadioBandwidth uint32 = 925000 22 | 23 | // channelByMinRadioCenterFreqency implements sort.Interface for []*gw.ChannelConfiguration. 24 | // The sorting is based on the center frequency of the radio when placing the 25 | // channel exactly on the left side of the available radio bandwidth. 26 | type channelByMinRadioCenterFrequency []*gw.ChannelConfiguration 27 | 28 | func (c channelByMinRadioCenterFrequency) Len() int { return len(c) } 29 | func (c channelByMinRadioCenterFrequency) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 30 | func (c channelByMinRadioCenterFrequency) Less(i, j int) bool { 31 | return c.minRadioCenterFreq(i) < c.minRadioCenterFreq(j) 32 | } 33 | func (c channelByMinRadioCenterFrequency) minRadioCenterFreq(i int) uint32 { 34 | var channelBandwidth uint32 35 | 36 | if lora := c[i].GetLoraModulationConfig(); lora != nil { 37 | channelBandwidth = lora.Bandwidth 38 | } 39 | 40 | if fsk := c[i].GetFskModulationConfig(); fsk != nil { 41 | channelBandwidth = fsk.Bandwidth 42 | } 43 | 44 | radioBandwidth, ok := radioBandwidthPerChannelBandwidth[channelBandwidth] 45 | if !ok { 46 | radioBandwidth = defaultRadioBandwidth 47 | } 48 | return c[i].Frequency - (channelBandwidth / 2) + (radioBandwidth / 2) 49 | } 50 | 51 | // GetRadioFrequencies returns the center-frequencies for the two radios. 52 | func GetRadioFrequencies(channels []*gw.ChannelConfiguration) ([2]uint32, error) { 53 | var radios [2]uint32 54 | 55 | // make sure the channels are sorted by the minimum radio center frequency 56 | sort.Sort(channelByMinRadioCenterFrequency(channels)) 57 | 58 | for _, c := range channels { 59 | var channelBandwidth uint32 60 | 61 | if lora := c.GetLoraModulationConfig(); lora != nil { 62 | channelBandwidth = lora.Bandwidth 63 | } 64 | 65 | if fsk := c.GetFskModulationConfig(); fsk != nil { 66 | channelBandwidth = fsk.Bandwidth 67 | } 68 | 69 | channelMax := c.Frequency + (channelBandwidth / 2) 70 | radioBandwidth, ok := radioBandwidthPerChannelBandwidth[channelBandwidth] 71 | if !ok { 72 | radioBandwidth = defaultRadioBandwidth 73 | } 74 | minRadioCenterFreq := c.Frequency - (channelBandwidth / 2) + (radioBandwidth / 2) 75 | 76 | for i := range radios { 77 | // the radio is not defined yet, use it 78 | if radios[i] == 0 { 79 | radios[i] = minRadioCenterFreq 80 | break 81 | } 82 | 83 | // channel fits within bandwidth of radio 84 | if channelMax <= radios[i]+(radioBandwidth/2) { 85 | break 86 | } 87 | 88 | // the channel does not fit 89 | if i == len(radios)-1 { 90 | return radios, fmt.Errorf("channel %d does not fit in radio bandwidth", c.Frequency) 91 | } 92 | } 93 | } 94 | 95 | return radios, nil 96 | } 97 | 98 | // GetRadioForChannel returns the radio number to which the channel must be assigned. 99 | func GetRadioForChannel(radios [2]uint32, c *gw.ChannelConfiguration) (int, error) { 100 | var channelBandwidth uint32 101 | 102 | if lora := c.GetLoraModulationConfig(); lora != nil { 103 | channelBandwidth = lora.Bandwidth 104 | } 105 | 106 | if fsk := c.GetFskModulationConfig(); fsk != nil { 107 | channelBandwidth = fsk.Bandwidth 108 | } 109 | 110 | channelMin := c.Frequency - (channelBandwidth / 2) 111 | channelMax := c.Frequency + (channelBandwidth / 2) 112 | radioBandwidth, ok := radioBandwidthPerChannelBandwidth[channelBandwidth] 113 | if !ok { 114 | radioBandwidth = defaultRadioBandwidth 115 | } 116 | 117 | for i, f := range radios { 118 | if channelMin >= f-(radioBandwidth/2) && channelMax <= f+(radioBandwidth/2) { 119 | return i, nil 120 | } 121 | } 122 | 123 | return 0, fmt.Errorf("channel %d does not fit in radio bandwidth", c.Frequency) 124 | } 125 | -------------------------------------------------------------------------------- /internal/filters/filters.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/pkg/errors" 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 10 | "github.com/brocaar/lorawan" 11 | ) 12 | 13 | var netIDs []lorawan.NetID 14 | var joinEUIs [][2]lorawan.EUI64 15 | 16 | // Setup configures the filters package. 17 | func Setup(conf config.Config) error { 18 | for _, netIDStr := range conf.Filters.NetIDs { 19 | var netID lorawan.NetID 20 | if err := netID.UnmarshalText([]byte(netIDStr)); err != nil { 21 | return errors.Wrap(err, "unmarshal NetID error") 22 | } 23 | 24 | netIDs = append(netIDs, netID) 25 | log.WithFields(log.Fields{ 26 | "net_id": netID, 27 | }).Info("filters: NetID filter configured") 28 | } 29 | 30 | for _, set := range conf.Filters.JoinEUIs { 31 | var joinEUISet [2]lorawan.EUI64 32 | 33 | for i, s := range set { 34 | var joinEUI lorawan.EUI64 35 | if err := joinEUI.UnmarshalText([]byte(s)); err != nil { 36 | return errors.Wrap(err, "unmarshal JoinEUI error") 37 | } 38 | 39 | joinEUISet[i] = joinEUI 40 | } 41 | 42 | joinEUIs = append(joinEUIs, joinEUISet) 43 | 44 | log.WithFields(log.Fields{ 45 | "join_eui_from": joinEUISet[0], 46 | "join_eui_to": joinEUISet[1], 47 | }).Info("filters: JoinEUI range configured") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // MatchFilters will match the given LoRaWAN frame against the configured 54 | // filters. This function returns true in the following cases: 55 | // * If the PHYPayload matches the configured filters 56 | // * If no filters are configured 57 | // * In case the PHYPayload is not a valid LoRaWAN frame 58 | func MatchFilters(b []byte) bool { 59 | // return true when no filters are configured 60 | if len(netIDs) == 0 && len(joinEUIs) == 0 { 61 | return true 62 | } 63 | 64 | // return true when we can't decode the LoRaWAN frame 65 | var phy lorawan.PHYPayload 66 | if err := phy.UnmarshalBinary(b); err != nil { 67 | log.WithError(err).Error("filters: unmarshal phypayload error") 68 | return true 69 | } 70 | 71 | switch phy.MHDR.MType { 72 | case lorawan.UnconfirmedDataUp, lorawan.ConfirmedDataUp: 73 | return filterDevAddr(phy) 74 | case lorawan.JoinRequest: 75 | return filterJoinRequest(phy) 76 | case lorawan.RejoinRequest: 77 | return filterRejoinRequest(phy) 78 | default: 79 | return true 80 | } 81 | } 82 | 83 | func matchNetIDFilter(netID lorawan.NetID) bool { 84 | if len(netIDs) == 0 { 85 | return true 86 | } 87 | 88 | for _, n := range netIDs { 89 | if n == netID { 90 | return true 91 | } 92 | } 93 | 94 | return false 95 | } 96 | 97 | func matchNetIDFilterForDevAddr(devAddr lorawan.DevAddr) bool { 98 | if len(netIDs) == 0 { 99 | return true 100 | } 101 | 102 | for _, netID := range netIDs { 103 | if devAddr.IsNetID(netID) { 104 | return true 105 | } 106 | } 107 | 108 | return false 109 | } 110 | 111 | func matchJoinEUIFilter(joinEUI lorawan.EUI64) bool { 112 | if len(joinEUIs) == 0 { 113 | return true 114 | } 115 | 116 | joinEUIInt := binary.BigEndian.Uint64(joinEUI[:]) 117 | 118 | for _, pair := range joinEUIs { 119 | min := binary.BigEndian.Uint64(pair[0][:]) 120 | max := binary.BigEndian.Uint64(pair[1][:]) 121 | 122 | if joinEUIInt >= min && joinEUIInt <= max { 123 | return true 124 | } 125 | } 126 | 127 | return false 128 | } 129 | 130 | func filterDevAddr(phy lorawan.PHYPayload) bool { 131 | mac, ok := phy.MACPayload.(*lorawan.MACPayload) 132 | if !ok { 133 | return true 134 | } 135 | 136 | return matchNetIDFilterForDevAddr(mac.FHDR.DevAddr) 137 | } 138 | 139 | func filterJoinRequest(phy lorawan.PHYPayload) bool { 140 | jr, ok := phy.MACPayload.(*lorawan.JoinRequestPayload) 141 | if !ok { 142 | return true 143 | } 144 | 145 | return matchJoinEUIFilter(jr.JoinEUI) 146 | } 147 | 148 | func filterRejoinRequest(phy lorawan.PHYPayload) bool { 149 | switch v := phy.MACPayload.(type) { 150 | case *lorawan.RejoinRequestType02Payload: 151 | return matchNetIDFilter(v.NetID) 152 | case *lorawan.RejoinRequestType1Payload: 153 | return matchJoinEUIFilter(v.JoinEUI) 154 | default: 155 | return true 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /internal/filters/filters_test.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 7 | "github.com/brocaar/lorawan" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFilters(t *testing.T) { 12 | netID0 := lorawan.NetID{0x00, 0x00, 0x00} 13 | devAddr00 := lorawan.DevAddr{0x01, 0x01, 0x01, 0x01} 14 | devAddr00.SetAddrPrefix(netID0) 15 | 16 | netID1 := lorawan.NetID{0x00, 0x00, 0x01} 17 | devAddr10 := lorawan.DevAddr{0x01, 0x01, 0x01, 0x01} 18 | devAddr10.SetAddrPrefix(netID1) 19 | 20 | tests := []struct { 21 | Name string 22 | NetIDFilters []string 23 | JoinEUIFilters [][2]string 24 | PHYPayload lorawan.PHYPayload 25 | Expected bool 26 | }{ 27 | { 28 | Name: "join-request, no filter", 29 | PHYPayload: lorawan.PHYPayload{ 30 | MHDR: lorawan.MHDR{ 31 | MType: lorawan.JoinRequest, 32 | Major: lorawan.LoRaWANR1, 33 | }, 34 | MACPayload: &lorawan.JoinRequestPayload{ 35 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 36 | }, 37 | }, 38 | Expected: true, 39 | }, 40 | { 41 | Name: "join-request matching JoinEUI - 1", 42 | JoinEUIFilters: [][2]string{ 43 | [2]string{"0000000000000001", "0000000000000002"}, 44 | }, 45 | PHYPayload: lorawan.PHYPayload{ 46 | MHDR: lorawan.MHDR{ 47 | MType: lorawan.JoinRequest, 48 | Major: lorawan.LoRaWANR1, 49 | }, 50 | MACPayload: &lorawan.JoinRequestPayload{ 51 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, 52 | }, 53 | }, 54 | Expected: true, 55 | }, 56 | { 57 | Name: "join-request matching JoinEUI - 2", 58 | JoinEUIFilters: [][2]string{ 59 | [2]string{"0000000000000001", "0000000000000002"}, 60 | }, 61 | PHYPayload: lorawan.PHYPayload{ 62 | MHDR: lorawan.MHDR{ 63 | MType: lorawan.JoinRequest, 64 | Major: lorawan.LoRaWANR1, 65 | }, 66 | MACPayload: &lorawan.JoinRequestPayload{ 67 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, 68 | }, 69 | }, 70 | Expected: true, 71 | }, 72 | { 73 | Name: "join-request not matching JoinEUI - 1", 74 | JoinEUIFilters: [][2]string{ 75 | [2]string{"0000000000000001", "0000000000000002"}, 76 | }, 77 | PHYPayload: lorawan.PHYPayload{ 78 | MHDR: lorawan.MHDR{ 79 | MType: lorawan.JoinRequest, 80 | Major: lorawan.LoRaWANR1, 81 | }, 82 | MACPayload: &lorawan.JoinRequestPayload{ 83 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 84 | }, 85 | }, 86 | Expected: false, 87 | }, 88 | { 89 | Name: "join-request not matching JoinEUI - 2", 90 | JoinEUIFilters: [][2]string{ 91 | [2]string{"0000000000000001", "0000000000000002"}, 92 | }, 93 | PHYPayload: lorawan.PHYPayload{ 94 | MHDR: lorawan.MHDR{ 95 | MType: lorawan.JoinRequest, 96 | Major: lorawan.LoRaWANR1, 97 | }, 98 | MACPayload: &lorawan.JoinRequestPayload{ 99 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}, 100 | }, 101 | }, 102 | Expected: false, 103 | }, 104 | { 105 | Name: "rejoin 1 not matching JoinEUI", 106 | JoinEUIFilters: [][2]string{ 107 | [2]string{"0000000000000001", "0000000000000002"}, 108 | }, 109 | PHYPayload: lorawan.PHYPayload{ 110 | MHDR: lorawan.MHDR{ 111 | MType: lorawan.RejoinRequest, 112 | Major: lorawan.LoRaWANR1, 113 | }, 114 | MACPayload: &lorawan.RejoinRequestType1Payload{ 115 | RejoinType: lorawan.RejoinRequestType1, 116 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}, 117 | }, 118 | }, 119 | Expected: false, 120 | }, 121 | { 122 | Name: "rejoin 1 matching JoinEUI", 123 | JoinEUIFilters: [][2]string{ 124 | [2]string{"0000000000000001", "0000000000000002"}, 125 | }, 126 | PHYPayload: lorawan.PHYPayload{ 127 | MHDR: lorawan.MHDR{ 128 | MType: lorawan.RejoinRequest, 129 | Major: lorawan.LoRaWANR1, 130 | }, 131 | MACPayload: &lorawan.RejoinRequestType1Payload{ 132 | RejoinType: lorawan.RejoinRequestType1, 133 | JoinEUI: lorawan.EUI64{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, 134 | }, 135 | }, 136 | Expected: true, 137 | }, 138 | { 139 | Name: "uplink data, no filter", 140 | PHYPayload: lorawan.PHYPayload{ 141 | MHDR: lorawan.MHDR{ 142 | MType: lorawan.UnconfirmedDataUp, 143 | Major: lorawan.LoRaWANR1, 144 | }, 145 | MACPayload: &lorawan.MACPayload{ 146 | FHDR: lorawan.FHDR{ 147 | DevAddr: devAddr00, 148 | }, 149 | }, 150 | }, 151 | Expected: true, 152 | }, 153 | { 154 | Name: "uplink data NetID match", 155 | NetIDFilters: []string{netID0.String()}, 156 | PHYPayload: lorawan.PHYPayload{ 157 | MHDR: lorawan.MHDR{ 158 | MType: lorawan.UnconfirmedDataUp, 159 | Major: lorawan.LoRaWANR1, 160 | }, 161 | MACPayload: &lorawan.MACPayload{ 162 | FHDR: lorawan.FHDR{ 163 | DevAddr: devAddr00, 164 | }, 165 | }, 166 | }, 167 | Expected: true, 168 | }, 169 | { 170 | Name: "uplink data NetID no match", 171 | NetIDFilters: []string{netID0.String()}, 172 | PHYPayload: lorawan.PHYPayload{ 173 | MHDR: lorawan.MHDR{ 174 | MType: lorawan.UnconfirmedDataUp, 175 | Major: lorawan.LoRaWANR1, 176 | }, 177 | MACPayload: &lorawan.MACPayload{ 178 | FHDR: lorawan.FHDR{ 179 | DevAddr: devAddr10, 180 | }, 181 | }, 182 | }, 183 | Expected: false, 184 | }, 185 | { 186 | Name: "rejoin request 0/2 NetID match", 187 | NetIDFilters: []string{netID0.String()}, 188 | PHYPayload: lorawan.PHYPayload{ 189 | MHDR: lorawan.MHDR{ 190 | MType: lorawan.RejoinRequest, 191 | Major: lorawan.LoRaWANR1, 192 | }, 193 | MACPayload: &lorawan.RejoinRequestType02Payload{ 194 | RejoinType: lorawan.RejoinRequestType0, 195 | NetID: netID0, 196 | }, 197 | }, 198 | Expected: true, 199 | }, 200 | { 201 | Name: "rejoin request 0/2 NetID match", 202 | NetIDFilters: []string{netID0.String()}, 203 | PHYPayload: lorawan.PHYPayload{ 204 | MHDR: lorawan.MHDR{ 205 | MType: lorawan.RejoinRequest, 206 | Major: lorawan.LoRaWANR1, 207 | }, 208 | MACPayload: &lorawan.RejoinRequestType02Payload{ 209 | RejoinType: lorawan.RejoinRequestType0, 210 | NetID: netID1, 211 | }, 212 | }, 213 | Expected: false, 214 | }, 215 | } 216 | 217 | for _, tst := range tests { 218 | t.Run(tst.Name, func(t *testing.T) { 219 | assert := require.New(t) 220 | 221 | netIDs = nil 222 | joinEUIs = nil 223 | 224 | var conf config.Config 225 | conf.Filters.NetIDs = tst.NetIDFilters 226 | conf.Filters.JoinEUIs = tst.JoinEUIFilters 227 | 228 | assert.NoError(Setup(conf)) 229 | 230 | b, err := tst.PHYPayload.MarshalBinary() 231 | assert.NoError(err) 232 | 233 | assert.Equal(tst.Expected, MatchFilters(b)) 234 | }) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /internal/forwarder/forwarder.go: -------------------------------------------------------------------------------- 1 | package forwarder 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | log "github.com/sirupsen/logrus" 6 | 7 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend" 8 | "github.com/brocaar/chirpstack-gateway-bridge/internal/backend/events" 9 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 10 | "github.com/brocaar/chirpstack-gateway-bridge/internal/integration" 11 | "github.com/brocaar/chirpstack-gateway-bridge/internal/metadata" 12 | "github.com/brocaar/lorawan" 13 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 14 | ) 15 | 16 | // Setup configures the forwarder. 17 | func Setup(conf config.Config) error { 18 | b := backend.GetBackend() 19 | i := integration.GetIntegration() 20 | 21 | if b == nil { 22 | return errors.New("backend is not set") 23 | } 24 | 25 | if i == nil { 26 | return errors.New("integration is not set") 27 | } 28 | 29 | // setup backend callbacks 30 | b.SetSubscribeEventFunc(gatewaySubscribeFunc) 31 | b.SetUplinkFrameFunc(uplinkFrameFunc) 32 | b.SetGatewayStatsFunc(gatewayStatsFunc) 33 | b.SetDownlinkTxAckFunc(downlinkTxAckFunc) 34 | b.SetRawPacketForwarderEventFunc(rawPacketForwarderEventFunc) 35 | 36 | // setup integration callbacks 37 | i.SetDownlinkFrameFunc(downlinkFrameFunc) 38 | i.SetGatewayConfigurationFunc(gatewayConfigurationFunc) 39 | i.SetRawPacketForwarderCommandFunc(rawPacketForwarderCommandFunc) 40 | 41 | return nil 42 | } 43 | 44 | func gatewaySubscribeFunc(pl events.Subscribe) { 45 | go func(pl events.Subscribe) { 46 | if err := integration.GetIntegration().SetGatewaySubscription(pl.Subscribe, pl.GatewayID); err != nil { 47 | log.WithError(err).Error("set gateway subscription error") 48 | } 49 | }(pl) 50 | } 51 | 52 | func uplinkFrameFunc(pl *gw.UplinkFrame) { 53 | go func(pl *gw.UplinkFrame) { 54 | var gatewayID lorawan.EUI64 55 | if err := gatewayID.UnmarshalText([]byte(pl.GetRxInfo().GetGatewayId())); err != nil { 56 | log.WithError(err).Error("decode gateway id error") 57 | return 58 | } 59 | 60 | if err := integration.GetIntegration().PublishEvent(gatewayID, integration.EventUp, pl.GetRxInfo().GetUplinkId(), pl); err != nil { 61 | log.WithError(err).WithFields(log.Fields{ 62 | "gateway_id": gatewayID, 63 | "event_type": integration.EventUp, 64 | "uplink_id": pl.GetRxInfo().GetUplinkId(), 65 | }).Error("publish event error") 66 | } 67 | }(pl) 68 | } 69 | 70 | func gatewayStatsFunc(pl *gw.GatewayStats) { 71 | go func(pl *gw.GatewayStats) { 72 | var gatewayID lorawan.EUI64 73 | if err := gatewayID.UnmarshalText([]byte(pl.GetGatewayId())); err != nil { 74 | log.WithError(err).Error("decode gateway id error") 75 | return 76 | } 77 | 78 | // add meta-data to stats 79 | if pl.Metadata == nil { 80 | pl.Metadata = make(map[string]string) 81 | } 82 | for k, v := range metadata.Get() { 83 | pl.Metadata[k] = v 84 | } 85 | 86 | if err := integration.GetIntegration().PublishEvent(gatewayID, integration.EventStats, 0, pl); err != nil { 87 | log.WithError(err).WithFields(log.Fields{ 88 | "gateway_id": gatewayID, 89 | "event_type": integration.EventStats, 90 | }).Error("publish event error") 91 | } 92 | }(pl) 93 | } 94 | 95 | func downlinkTxAckFunc(pl *gw.DownlinkTxAck) { 96 | go func(pl *gw.DownlinkTxAck) { 97 | var gatewayID lorawan.EUI64 98 | if err := gatewayID.UnmarshalText([]byte(pl.GetGatewayId())); err != nil { 99 | log.WithError(err).Error("decode gateway id error") 100 | return 101 | } 102 | 103 | if err := integration.GetIntegration().PublishEvent(gatewayID, integration.EventAck, pl.GetDownlinkId(), pl); err != nil { 104 | log.WithError(err).WithFields(log.Fields{ 105 | "gateway_id": gatewayID, 106 | "event_type": integration.EventAck, 107 | "downlink_id": pl.GetDownlinkId(), 108 | }).Error("publish event error") 109 | } 110 | }(pl) 111 | } 112 | 113 | func rawPacketForwarderEventFunc(pl *gw.RawPacketForwarderEvent) { 114 | go func(pl *gw.RawPacketForwarderEvent) { 115 | var gatewayID lorawan.EUI64 116 | if err := gatewayID.UnmarshalText([]byte(pl.GetGatewayId())); err != nil { 117 | log.WithError(err).Error("decode gateway id error") 118 | return 119 | } 120 | 121 | if err := integration.GetIntegration().PublishEvent(gatewayID, integration.EventRaw, 0, pl); err != nil { 122 | log.WithError(err).WithFields(log.Fields{ 123 | "gateway_id": gatewayID, 124 | "event_type": integration.EventRaw, 125 | }).Error("publish event error") 126 | } 127 | }(pl) 128 | } 129 | 130 | func downlinkFrameFunc(pl *gw.DownlinkFrame) { 131 | go func(pl *gw.DownlinkFrame) { 132 | if err := backend.GetBackend().SendDownlinkFrame(pl); err != nil { 133 | log.WithError(err).Error("send downlink frame error") 134 | } 135 | }(pl) 136 | } 137 | 138 | func gatewayConfigurationFunc(pl *gw.GatewayConfiguration) { 139 | go func(pl *gw.GatewayConfiguration) { 140 | if err := backend.GetBackend().ApplyConfiguration(pl); err != nil { 141 | log.WithError(err).Error("apply gateway-configuration error") 142 | } 143 | }(pl) 144 | } 145 | 146 | func rawPacketForwarderCommandFunc(pl *gw.RawPacketForwarderCommand) { 147 | go func(pl *gw.RawPacketForwarderCommand) { 148 | if err := backend.GetBackend().RawPacketForwarderCommand(pl); err != nil { 149 | log.WithError(err).Error("raw packet-forwarder command error") 150 | } 151 | }(pl) 152 | } 153 | -------------------------------------------------------------------------------- /internal/integration/integration.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "google.golang.org/protobuf/proto" 6 | 7 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 8 | "github.com/brocaar/chirpstack-gateway-bridge/internal/integration/mqtt" 9 | "github.com/brocaar/lorawan" 10 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 11 | ) 12 | 13 | // Event types. 14 | const ( 15 | EventUp = "up" 16 | EventStats = "stats" 17 | EventAck = "ack" 18 | EventRaw = "raw" 19 | ) 20 | 21 | var integration Integration 22 | 23 | // Setup configures the integration. 24 | func Setup(conf config.Config) error { 25 | var err error 26 | integration, err = mqtt.NewBackend(conf) 27 | if err != nil { 28 | return errors.Wrap(err, "setup mqtt integration error") 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // GetIntegration returns the integration. 35 | func GetIntegration() Integration { 36 | return integration 37 | } 38 | 39 | // Integration defines the interface that an integration must implement. 40 | type Integration interface { 41 | // SetGatewaySubscription updates the gateway subscription for the given 42 | // gateway ID. The integration must implement this such that it is safe 43 | // to call the same action multiple times. 44 | SetGatewaySubscription(subscribe bool, gatewayID lorawan.EUI64) error 45 | 46 | // PublishEvent publishes the given event. 47 | PublishEvent(lorawan.EUI64, string, uint32, proto.Message) error 48 | 49 | // PublishState publishes the given state as retained message. 50 | PublishState(lorawan.EUI64, string, proto.Message) error 51 | 52 | // SetDownlinkFrameFunc sets the DownlinkFrame handler func. 53 | SetDownlinkFrameFunc(func(*gw.DownlinkFrame)) 54 | 55 | // SetRawPacketForwarderCommandFunc sets the RawPacketForwarderCommand handler func. 56 | SetRawPacketForwarderCommandFunc(func(*gw.RawPacketForwarderCommand)) 57 | 58 | // SetGatewayConfigurationFunc sets the GatewayConfiguration handler func. 59 | SetGatewayConfigurationFunc(func(*gw.GatewayConfiguration)) 60 | 61 | // SetGatewayCommandExecRequestFunc sets the GatewayCommandExecRequest handler func. 62 | SetGatewayCommandExecRequestFunc(func(*gw.GatewayCommandExecRequest)) 63 | 64 | // Start starts the integration. 65 | Start() error 66 | 67 | // Stop stops the integration. 68 | Stop() error 69 | } 70 | -------------------------------------------------------------------------------- /internal/integration/mqtt/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "time" 8 | 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/brocaar/lorawan" 13 | ) 14 | 15 | // Authentication defines the authentication interface. 16 | type Authentication interface { 17 | // Init applies the initial configuration. 18 | Init(*mqtt.ClientOptions) error 19 | 20 | // GetGatewayID returns the GatewayID if available. 21 | GetGatewayID() *lorawan.EUI64 22 | 23 | // Update updates the authentication options. 24 | Update(*mqtt.ClientOptions) error 25 | 26 | // ReconnectAfter returns a time.Duration after which the MQTT client must re-connect. 27 | // Note: return 0 to disable the periodical re-connect feature. 28 | ReconnectAfter() time.Duration 29 | } 30 | 31 | func newTLSConfig(cafile, certFile, certKeyFile string) (*tls.Config, error) { 32 | if cafile == "" && certFile == "" && certKeyFile == "" { 33 | return nil, nil 34 | } 35 | 36 | tlsConfig := &tls.Config{} 37 | 38 | if cafile != "" { 39 | cacert, err := ioutil.ReadFile(cafile) 40 | if err != nil { 41 | return nil, errors.Wrap(err, "load ca-cert error") 42 | } 43 | certpool := x509.NewCertPool() 44 | certpool.AppendCertsFromPEM(cacert) 45 | 46 | tlsConfig.RootCAs = certpool // RootCAs = certs used to verify server cert. 47 | } 48 | 49 | if certFile != "" && certKeyFile != "" { 50 | kp, err := tls.LoadX509KeyPair(certFile, certKeyFile) 51 | if err != nil { 52 | return nil, errors.Wrap(err, "load tls key-pair error") 53 | } 54 | tlsConfig.Certificates = []tls.Certificate{kp} 55 | } 56 | 57 | return tlsConfig, nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/integration/mqtt/auth/azure_iot_hub_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParseConnectionString(t *testing.T) { 11 | tests := []struct { 12 | Name string 13 | ConnectionString string 14 | ExpectedKV map[string]string 15 | ExpectedError error 16 | }{ 17 | { 18 | Name: "valid string", 19 | ConnectionString: "HostName=gateways-eu868.azure-devices.net;DeviceId=00800000a00016b6;SharedAccessKey=WWVQv+auegGaG2mm2/0FIS24xqkmZW/z5cYBO898+8I=", 20 | ExpectedKV: map[string]string{ 21 | "HostName": "gateways-eu868.azure-devices.net", 22 | "DeviceId": "00800000a00016b6", 23 | "SharedAccessKey": "WWVQv+auegGaG2mm2/0FIS24xqkmZW/z5cYBO898+8I=", 24 | }, 25 | }, 26 | { 27 | Name: "invalid string", 28 | ConnectionString: "HostName;gateways-eu868.azure-devices.net;DeviceId=00800000a00016b6;SharedAccessKey=WWVQv+auegGaG2mm2/0FIS24xqkmZW/z5cYBO898+8I=", 29 | ExpectedError: errors.New("expected two items in: [HostName]"), 30 | }, 31 | } 32 | 33 | for _, tst := range tests { 34 | t.Run(tst.Name, func(t *testing.T) { 35 | assert := require.New(t) 36 | 37 | kv, err := parseConnectionString(tst.ConnectionString) 38 | assert.Equal(tst.ExpectedError, err) 39 | if err != nil { 40 | return 41 | } 42 | 43 | assert.EqualValues(tst.ExpectedKV, kv) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/integration/mqtt/auth/gcp_cloud_iot_core.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rsa" 5 | "fmt" 6 | "io/ioutil" 7 | "time" 8 | 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | jwt "github.com/golang-jwt/jwt/v4" 11 | "github.com/pkg/errors" 12 | 13 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 14 | "github.com/brocaar/lorawan" 15 | ) 16 | 17 | // GCPCloudIoTCoreAuthentication implements the Google Cloud IoT Core authentication. 18 | type GCPCloudIoTCoreAuthentication struct { 19 | siginingMethod *jwt.SigningMethodRSA 20 | privateKey *rsa.PrivateKey 21 | clientID string 22 | server string 23 | projectID string 24 | jwtExpiration time.Duration 25 | } 26 | 27 | // NewGCPCloudIoTCoreAuthentication create a GCPCloudIoTCoreAuthentication. 28 | func NewGCPCloudIoTCoreAuthentication(conf config.Config) (Authentication, error) { 29 | keyFileRaw, err := ioutil.ReadFile(conf.Integration.MQTT.Auth.GCPCloudIoTCore.JWTKeyFile) 30 | if err != nil { 31 | return nil, errors.Wrap(err, "read jwt key-file error") 32 | } 33 | 34 | privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyFileRaw) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "parse jwt key-file error") 37 | } 38 | 39 | clientID := fmt.Sprintf("projects/%s/locations/%s/registries/%s/devices/%s", 40 | conf.Integration.MQTT.Auth.GCPCloudIoTCore.ProjectID, 41 | conf.Integration.MQTT.Auth.GCPCloudIoTCore.CloudRegion, 42 | conf.Integration.MQTT.Auth.GCPCloudIoTCore.RegistryID, 43 | conf.Integration.MQTT.Auth.GCPCloudIoTCore.DeviceID, 44 | ) 45 | 46 | return &GCPCloudIoTCoreAuthentication{ 47 | siginingMethod: jwt.SigningMethodRS256, 48 | privateKey: privateKey, 49 | clientID: clientID, 50 | server: conf.Integration.MQTT.Auth.GCPCloudIoTCore.Server, 51 | projectID: conf.Integration.MQTT.Auth.GCPCloudIoTCore.ProjectID, 52 | jwtExpiration: conf.Integration.MQTT.Auth.GCPCloudIoTCore.JWTExpiration, 53 | }, nil 54 | } 55 | 56 | // Init applies the initial configuration. 57 | func (a *GCPCloudIoTCoreAuthentication) Init(opts *mqtt.ClientOptions) error { 58 | opts.AddBroker(a.server) 59 | opts.SetClientID(a.clientID) 60 | return nil 61 | } 62 | 63 | // GetGatewayID returns the GatewayID if available. 64 | // TODO: implement. 65 | func (a *GCPCloudIoTCoreAuthentication) GetGatewayID() *lorawan.EUI64 { 66 | return nil 67 | } 68 | 69 | // Update updates the authentication options. 70 | func (a *GCPCloudIoTCoreAuthentication) Update(opts *mqtt.ClientOptions) error { 71 | token := jwt.NewWithClaims(a.siginingMethod, jwt.StandardClaims{ 72 | IssuedAt: time.Now().Unix(), 73 | ExpiresAt: time.Now().Add(a.ReconnectAfter()).Unix(), 74 | Audience: a.projectID, 75 | }) 76 | 77 | signedToken, err := token.SignedString(a.privateKey) 78 | if err != nil { 79 | return errors.Wrap(err, "sign jwt token error") 80 | } 81 | 82 | opts.SetUsername(signedToken) 83 | opts.SetPassword(signedToken) 84 | 85 | return nil 86 | } 87 | 88 | // ReconnectAfter returns a time.Duration after which the MQTT.Auth.client must re-connect. 89 | // Note: return 0 to disable the periodical re-connect feature. 90 | func (a *GCPCloudIoTCoreAuthentication) ReconnectAfter() time.Duration { 91 | return a.jwtExpiration 92 | } 93 | -------------------------------------------------------------------------------- /internal/integration/mqtt/auth/generic.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | 7 | mqtt "github.com/eclipse/paho.mqtt.golang" 8 | "github.com/pkg/errors" 9 | log "github.com/sirupsen/logrus" 10 | 11 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 12 | "github.com/brocaar/lorawan" 13 | ) 14 | 15 | // GenericAuthentication implements a generic MQTT authentication. 16 | type GenericAuthentication struct { 17 | servers []string 18 | username string 19 | password string 20 | cleanSession bool 21 | clientID string 22 | 23 | tlsConfig *tls.Config 24 | } 25 | 26 | // NewGenericAuthentication creates a GenericAuthentication. 27 | func NewGenericAuthentication(conf config.Config) (Authentication, error) { 28 | tlsConfig, err := newTLSConfig( 29 | conf.Integration.MQTT.Auth.Generic.CACert, 30 | conf.Integration.MQTT.Auth.Generic.TLSCert, 31 | conf.Integration.MQTT.Auth.Generic.TLSKey, 32 | ) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "mqtt/auth: new tls config error") 35 | } 36 | 37 | return &GenericAuthentication{ 38 | tlsConfig: tlsConfig, 39 | servers: conf.Integration.MQTT.Auth.Generic.Servers, 40 | username: conf.Integration.MQTT.Auth.Generic.Username, 41 | password: conf.Integration.MQTT.Auth.Generic.Password, 42 | cleanSession: conf.Integration.MQTT.Auth.Generic.CleanSession, 43 | clientID: conf.Integration.MQTT.Auth.Generic.ClientID, 44 | }, nil 45 | } 46 | 47 | // Init applies the initial configuration. 48 | func (a *GenericAuthentication) Init(opts *mqtt.ClientOptions) error { 49 | for _, server := range a.servers { 50 | opts.AddBroker(server) 51 | } 52 | opts.SetUsername(a.username) 53 | opts.SetPassword(a.password) 54 | opts.SetCleanSession(a.cleanSession) 55 | opts.SetClientID(a.clientID) 56 | 57 | if a.tlsConfig != nil { 58 | opts.SetTLSConfig(a.tlsConfig) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // GetGatewayID returns the GatewayID if available. 65 | func (a *GenericAuthentication) GetGatewayID() *lorawan.EUI64 { 66 | if a.clientID == "" { 67 | return nil 68 | } 69 | 70 | // Try to decode the client ID as gateway ID. 71 | var gatewayID lorawan.EUI64 72 | if err := gatewayID.UnmarshalText([]byte(a.clientID)); err != nil { 73 | log.WithError(err).WithFields(log.Fields{ 74 | "client_id": a.clientID, 75 | }).Warning("integration/mqtt/auth: could not decode client ID to gateway ID") 76 | return nil 77 | } 78 | 79 | return &gatewayID 80 | } 81 | 82 | // Update updates the authentication options. 83 | func (a *GenericAuthentication) Update(opts *mqtt.ClientOptions) error { 84 | return nil 85 | } 86 | 87 | // ReconnectAfter returns a time.Duration after which the MQTT client must re-connect. 88 | // Note: return 0 to disable the periodical re-connect feature. 89 | func (a *GenericAuthentication) ReconnectAfter() time.Duration { 90 | return 0 91 | } 92 | -------------------------------------------------------------------------------- /internal/integration/mqtt/auth/generic_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 7 | "github.com/brocaar/lorawan" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGenericAuthentication(t *testing.T) { 12 | gatewayID := lorawan.EUI64{1, 2, 3, 4, 5, 6, 7, 8} 13 | 14 | var conf config.Config 15 | conf.Integration.Marshaler = "json" 16 | conf.Integration.MQTT.EventTopicTemplate = "gateway/{{ .GatewayID }}/event/{{ .EventType }}" 17 | conf.Integration.MQTT.StateTopicTemplate = "gateway/{{ .GatewayID }}/state/{{ .StateType }}" 18 | conf.Integration.MQTT.CommandTopicTemplate = "gateway/{{ .GatewayID }}/command/#" 19 | conf.Integration.MQTT.Auth.Type = "generic" 20 | conf.Integration.MQTT.Auth.Generic.Servers = []string{"tcp://localhost:1883"} 21 | conf.Integration.MQTT.Auth.Generic.Username = "foo" 22 | conf.Integration.MQTT.Auth.Generic.Password = "bar" 23 | conf.Integration.MQTT.Auth.Generic.CleanSession = true 24 | conf.Integration.MQTT.Auth.Generic.ClientID = gatewayID.String() 25 | 26 | t.Run("New", func(t *testing.T) { 27 | assert := require.New(t) 28 | 29 | auth, err := NewGenericAuthentication(conf) 30 | assert.NoError(err) 31 | 32 | t.Run("GetGatewayID", func(t *testing.T) { 33 | assert := require.New(t) 34 | assert.Equal(&gatewayID, auth.GetGatewayID()) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/integration/mqtt/metrics.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | pc = promauto.NewCounterVec(prometheus.CounterOpts{ 10 | Name: "integration_mqtt_event_count", 11 | Help: "The number of gateway events published by the MQTT integration (per event).", 12 | }, []string{"event"}) 13 | 14 | sc = promauto.NewCounterVec(prometheus.CounterOpts{ 15 | Name: "integration_mqtt_state_count", 16 | Help: "The number of gateway states published by the MQTT integration (per state).", 17 | }, []string{"state"}) 18 | 19 | cc = promauto.NewCounterVec(prometheus.CounterOpts{ 20 | Name: "integration_mqtt_command_count", 21 | Help: "The number of commands received by the MQTT integration (per command).", 22 | }, []string{"command"}) 23 | 24 | mqttc = promauto.NewCounter(prometheus.CounterOpts{ 25 | Name: "integration_mqtt_connect_count", 26 | Help: "The number of times the integration connected to the MQTT broker.", 27 | }) 28 | 29 | mqttd = promauto.NewCounter(prometheus.CounterOpts{ 30 | Name: "integration_mqtt_disconnect_count", 31 | Help: "The number of times the integration disconnected from the MQTT broker.", 32 | }) 33 | 34 | mqttr = promauto.NewCounter(prometheus.CounterOpts{ 35 | Name: "integration_mqtt_reconnect_count", 36 | Help: "The number of times the integration reconnected to the MQTT broker (this also increments the disconnect and connect counters).", 37 | }) 38 | ) 39 | 40 | func mqttEventCounter(e string) prometheus.Counter { 41 | return pc.With(prometheus.Labels{"event": e}) 42 | } 43 | 44 | func mqttStateCounter(s string) prometheus.Counter { 45 | return sc.With(prometheus.Labels{"state": s}) 46 | } 47 | 48 | func mqttCommandCounter(c string) prometheus.Counter { 49 | return cc.With(prometheus.Labels{"command": c}) 50 | } 51 | 52 | func mqttConnectCounter() prometheus.Counter { 53 | return mqttc 54 | } 55 | 56 | func mqttDisconnectCounter() prometheus.Counter { 57 | return mqttd 58 | } 59 | 60 | func mqttReconnectCounter() prometheus.Counter { 61 | return mqttr 62 | } 63 | -------------------------------------------------------------------------------- /internal/metadata/metadata.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "strings" 7 | "sync" 8 | "time" 9 | "unicode/utf8" 10 | 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/brocaar/chirpstack-gateway-bridge/internal/commands" 15 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 16 | ) 17 | 18 | var ( 19 | mux sync.RWMutex 20 | 21 | static map[string]string 22 | cmnds map[string]string 23 | cached map[string]string 24 | 25 | interval time.Duration 26 | maxExecution time.Duration 27 | splitDelimiter string 28 | ) 29 | 30 | // Setup configures the metadata package. 31 | func Setup(conf config.Config) error { 32 | mux.Lock() 33 | defer mux.Unlock() 34 | 35 | static = conf.MetaData.Static 36 | cmnds = conf.MetaData.Dynamic.Commands 37 | 38 | interval = conf.MetaData.Dynamic.ExecutionInterval 39 | maxExecution = conf.MetaData.Dynamic.MaxExecutionDuration 40 | splitDelimiter = conf.MetaData.Dynamic.SplitDelimiter 41 | 42 | go func() { 43 | for { 44 | runCommands() 45 | time.Sleep(interval) 46 | } 47 | }() 48 | 49 | return nil 50 | } 51 | 52 | // Get returns the (cached) metadata. 53 | func Get() map[string]string { 54 | mux.RLock() 55 | defer mux.RUnlock() 56 | 57 | return cached 58 | } 59 | 60 | func runCommands() { 61 | newKV := make(map[string]string) 62 | for k, v := range static { 63 | newKV[k] = v 64 | } 65 | 66 | for k, cmd := range cmnds { 67 | out, err := runCommand(cmd) 68 | if err != nil { 69 | log.WithError(err).WithFields(log.Fields{ 70 | "key": k, 71 | "cmd": cmd, 72 | }).Error("metadata: execute command error") 73 | continue 74 | } 75 | 76 | if strings.Contains(out, "\n") { 77 | rows := strings.Split(out, "\n") 78 | for _, row := range rows { 79 | kv := strings.SplitN(row, splitDelimiter, 2) 80 | if len(kv) != 2 { 81 | log.WithFields(log.Fields{ 82 | "row": row, 83 | "split_delimiter": splitDelimiter, 84 | }).Warning("metadata: can not split output in key / value") 85 | } else { 86 | newKV[k+"_"+kv[0]] = kv[1] 87 | } 88 | } 89 | 90 | } else { 91 | newKV[k] = out 92 | } 93 | } 94 | 95 | mux.Lock() 96 | defer mux.Unlock() 97 | cached = newKV 98 | } 99 | 100 | func runCommand(cmdStr string) (string, error) { 101 | cmdArgs, err := commands.ParseCommandLine(cmdStr) 102 | if err != nil { 103 | return "", errors.Wrap(err, "parse command error") 104 | } 105 | if len(cmdArgs) == 0 { 106 | return "", errors.New("no command is given") 107 | } 108 | 109 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(maxExecution)) 110 | defer cancel() 111 | 112 | cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) 113 | out, err := cmd.Output() 114 | if err != nil { 115 | return "", errors.Wrap(err, "execution error") 116 | } 117 | 118 | if !utf8.Valid(out) { 119 | return "", errors.New("command did not return valid utf8 string") 120 | } 121 | 122 | return strings.TrimRight(string(out), "\n\r"), nil 123 | } 124 | -------------------------------------------------------------------------------- /internal/metadata/metadata_test.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRunCommand(t *testing.T) { 12 | assert := require.New(t) 13 | 14 | tests := []struct { 15 | In string 16 | Out string 17 | MaxExecution time.Duration 18 | Error error 19 | }{ 20 | { 21 | In: "echo foo bar", 22 | Out: "foo bar", 23 | MaxExecution: time.Second, 24 | }, 25 | { 26 | In: "sleep 2", 27 | MaxExecution: time.Second, 28 | Error: errors.New("execution error: signal: killed"), 29 | }, 30 | } 31 | 32 | for _, tst := range tests { 33 | maxExecution = tst.MaxExecution 34 | out, err := runCommand(tst.In) 35 | if err != nil || tst.Error != nil { 36 | assert.Equal(tst.Error.Error(), err.Error()) 37 | } 38 | if err != nil { 39 | continue 40 | } 41 | assert.Equal(tst.Out, out) 42 | } 43 | } 44 | 45 | func TestMetaData(t *testing.T) { 46 | tests := []struct { 47 | Name string 48 | Static map[string]string 49 | Commands map[string]string 50 | Expected map[string]string 51 | }{ 52 | { 53 | Name: "static only", 54 | Static: map[string]string{ 55 | "foo": "test1", 56 | "bar": "test2", 57 | }, 58 | Expected: map[string]string{ 59 | "foo": "test1", 60 | "bar": "test2", 61 | }, 62 | }, 63 | { 64 | Name: "commands only", 65 | Commands: map[string]string{ 66 | "foo": "echo test1", 67 | "bar": "echo test2", 68 | }, 69 | Expected: map[string]string{ 70 | "foo": "test1", 71 | "bar": "test2", 72 | }, 73 | }, 74 | { 75 | Name: "static + commands", 76 | Static: map[string]string{ 77 | "static_1": "static 1", 78 | "static_2": "static_2", 79 | }, 80 | Commands: map[string]string{ 81 | "cmd_1": "echo cmd1", 82 | "cmd_2": "echo cmd2", 83 | }, 84 | Expected: map[string]string{ 85 | "static_1": "static 1", 86 | "static_2": "static_2", 87 | "cmd_1": "cmd1", 88 | "cmd_2": "cmd2", 89 | }, 90 | }, 91 | { 92 | Name: "command overwrites static", 93 | Static: map[string]string{ 94 | "foo": "test1", 95 | "bar": "test2", 96 | }, 97 | Commands: map[string]string{ 98 | "bar": "echo cmd overwrite", 99 | }, 100 | Expected: map[string]string{ 101 | "foo": "test1", 102 | "bar": "cmd overwrite", 103 | }, 104 | }, 105 | { 106 | Name: "command overwrites but timeout", 107 | Static: map[string]string{ 108 | "foo": "test1", 109 | "bar": "test2", 110 | }, 111 | Commands: map[string]string{ 112 | "bar": "sleep 2", 113 | }, 114 | Expected: map[string]string{ 115 | "foo": "test1", 116 | "bar": "test2", 117 | }, 118 | }, 119 | { 120 | Name: "command returns multiple rows", 121 | Commands: map[string]string{ 122 | "bar": `echo -e "foo=bar\nalice=bob\nsum=1+2=3"`, 123 | }, 124 | Expected: map[string]string{ 125 | "bar_foo": "bar", 126 | "bar_alice": "bob", 127 | "bar_sum": "1+2=3", 128 | }, 129 | }, 130 | } 131 | 132 | maxExecution = time.Second 133 | splitDelimiter = "=" 134 | 135 | for _, tst := range tests { 136 | t.Run(tst.Name, func(t *testing.T) { 137 | assert := require.New(t) 138 | 139 | static = tst.Static 140 | cmnds = tst.Commands 141 | 142 | runCommands() 143 | 144 | assert.EqualValues(tst.Expected, Get()) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/brocaar/chirpstack-gateway-bridge/internal/config" 10 | ) 11 | 12 | // Setup configures the metrics package. 13 | func Setup(conf config.Config) error { 14 | if !conf.Metrics.Prometheus.EndpointEnabled { 15 | return nil 16 | } 17 | 18 | log.WithFields(log.Fields{ 19 | "bind": conf.Metrics.Prometheus.Bind, 20 | }).Info("metrics: starting prometheus metrics server") 21 | 22 | server := http.Server{ 23 | Handler: promhttp.Handler(), 24 | Addr: conf.Metrics.Prometheus.Bind, 25 | } 26 | 27 | go func() { 28 | err := server.ListenAndServe() 29 | log.WithError(err).Error("metrics: prometheus metrics server error") 30 | }() 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/goreleaser/goreleaser" 7 | _ "github.com/goreleaser/nfpm" 8 | _ "golang.org/x/lint/golint" 9 | ) 10 | -------------------------------------------------------------------------------- /packaging/files/chirpstack-gateway-bridge.init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### BEGIN INIT INFO 3 | # Provides: chirpstack-gateway-bridge 4 | # Required-Start: $all 5 | # Required-Stop: $remote_fs $syslog 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: ChirpStack Gateway Bridge 9 | ### END INIT INFO 10 | 11 | 12 | NAME=chirpstack-gateway-bridge 13 | DESC="ChirpStack Gateway Bridge" 14 | DAEMON_USER=gatewaybridge 15 | DAEMON_GROUP=gatewaybridge 16 | DAEMON=/usr/bin/$NAME 17 | PID_FILE=/var/run/$NAME.pid 18 | 19 | 20 | # check root 21 | if [ "$UID" != "0" ]; then 22 | echo "You must be root to run this script" 23 | exit 1 24 | fi 25 | 26 | # check daemon 27 | if [ ! -x $DAEMON ]; then 28 | echo "Executable $DAEMON does not exist" 29 | exit 5 30 | fi 31 | 32 | # load functions and settings 33 | . /lib/lsb/init-functions 34 | 35 | if [ -r /etc/default/rcS ]; then 36 | . /etc/default/rcS 37 | fi 38 | 39 | function do_start { 40 | start-stop-daemon --start --background --chuid "$DAEMON_USER:$DAEMON_GROUP" --make-pidfile --pidfile "$PID_FILE" --startas /bin/bash -- -c "exec $DAEMON >> /var/log/$NAME/$NAME.log 2>&1" 41 | } 42 | 43 | function do_stop { 44 | start-stop-daemon --stop --retry=TERM/30/KILL/5 --pidfile "$PID_FILE" --exec "$DAEMON" 45 | retval="$?" 46 | sleep 1 47 | return "$retval" 48 | } 49 | 50 | case "$1" in 51 | start) 52 | log_daemon_msg "Starting $DESC" 53 | do_start 54 | case "$?" in 55 | 0|1) log_end_msg 0 ;; 56 | 2) log_end_msg 1 ;; 57 | esac 58 | ;; 59 | stop) 60 | log_daemon_msg "Stopping $DESC" 61 | do_stop 62 | case "$?" in 63 | 0|1) log_end_msg 0 ;; 64 | 2) log_end_msg 1 ;; 65 | esac 66 | ;; 67 | restart) 68 | log_daemon_msg "Restarting $DESC" 69 | do_stop 70 | case "$?" in 71 | 0|1) 72 | do_start 73 | case "$?" in 74 | 0) log_end_msg 0 ;; 75 | 1) log_end_msg 1 ;; 76 | *) log_end_msg 1 ;; 77 | esac 78 | ;; 79 | *) 80 | log_end_msg 1 81 | ;; 82 | esac 83 | ;; 84 | status) 85 | status_of_proc -p "$PID_FILE" "$DAEMON" "$NAME" && exit 0 || exit $? 86 | ;; 87 | *) 88 | echo "Usage: $NAME {start|stop|restart|status}" >&2 89 | exit 3 90 | ;; 91 | esac 92 | -------------------------------------------------------------------------------- /packaging/files/chirpstack-gateway-bridge.rotate: -------------------------------------------------------------------------------- 1 | /var/log/chirpstack-gateway-bridge/chirpstack-gateway-bridge.log { 2 | daily 3 | rotate 7 4 | missingok 5 | dateext 6 | copytruncate 7 | compress 8 | } 9 | -------------------------------------------------------------------------------- /packaging/files/chirpstack-gateway-bridge.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ChirpStack Gateway Bridge 3 | Documentation=https://www.chirpstack.io/ 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Service] 8 | User=gatewaybridge 9 | Group=gatewaybridge 10 | ExecStart=/usr/bin/chirpstack-gateway-bridge 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | Alias=lora-gateway-bridge.service 16 | -------------------------------------------------------------------------------- /packaging/files/chirpstack-gateway-bridge.toml: -------------------------------------------------------------------------------- 1 | # This configuration provides a Semtech UDP packet-forwarder backend and 2 | # integrates with a MQTT broker. Many options and defaults have been omitted 3 | # for simplicity. 4 | # 5 | # See https://www.chirpstack.io/gateway-bridge/install/config/ for a full 6 | # configuration example and documentation. 7 | 8 | 9 | # Gateway backend configuration. 10 | [backend] 11 | # Backend type. 12 | type="semtech_udp" 13 | 14 | # Semtech UDP packet-forwarder backend. 15 | [backend.semtech_udp] 16 | 17 | # ip:port to bind the UDP listener to 18 | # 19 | # Example: 0.0.0.0:1700 to listen on port 1700 for all network interfaces. 20 | # This is the listener to which the packet-forwarder forwards its data 21 | # so make sure the 'serv_port_up' and 'serv_port_down' from your 22 | # packet-forwarder matches this port. 23 | udp_bind = "0.0.0.0:1700" 24 | 25 | 26 | # Integration configuration. 27 | [integration] 28 | # Payload marshaler. 29 | # 30 | # This defines how the MQTT payloads are encoded. Valid options are: 31 | # * protobuf: Protobuf encoding 32 | # * json: JSON encoding (easier for debugging, but less compact than 'protobuf') 33 | marshaler="protobuf" 34 | 35 | # MQTT integration configuration. 36 | [integration.mqtt] 37 | # Event topic template. 38 | event_topic_template="gateway/{{ .GatewayID }}/event/{{ .EventType }}" 39 | 40 | # Command topic template. 41 | command_topic_template="gateway/{{ .GatewayID }}/command/#" 42 | 43 | # MQTT authentication. 44 | [integration.mqtt.auth] 45 | # Type defines the MQTT authentication type to use. 46 | # 47 | # Set this to the name of one of the sections below. 48 | type="generic" 49 | 50 | # Generic MQTT authentication. 51 | [integration.mqtt.auth.generic] 52 | # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) 53 | server="tcp://127.0.0.1:1883" 54 | 55 | # Connect with the given username (optional) 56 | username="" 57 | 58 | # Connect with the given password (optional) 59 | password="" 60 | 61 | -------------------------------------------------------------------------------- /packaging/scripts/compress-mips.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Compressing MIPS binary" 4 | upx dist/linux_mips/chirpstack-gateway-bridge 5 | -------------------------------------------------------------------------------- /packaging/scripts/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OLD_NAME=lora-gateway-bridge 4 | NAME=chirpstack-gateway-bridge 5 | BIN_DIR=/usr/bin 6 | SCRIPT_DIR=/usr/lib/chirpstack-gateway-bridge/scripts 7 | LOG_DIR=/var/log/chirpstack-gateway-bridge 8 | DAEMON_USER=gatewaybridge 9 | DAEMON_GROUP=gatewaybridge 10 | 11 | function install_init { 12 | cp -f $SCRIPT_DIR/$NAME.init /etc/init.d/$NAME 13 | chmod +x /etc/init.d/$NAME 14 | ln -s /etc/init.d/$NAME /etc/init.d/$OLD_NAME 15 | update-rc.d $NAME defaults 16 | } 17 | 18 | function install_systemd { 19 | cp -f $SCRIPT_DIR/$NAME.service /lib/systemd/system/$NAME.service 20 | systemctl daemon-reload 21 | systemctl enable $NAME 22 | } 23 | 24 | function restart_service { 25 | echo "Restarting $NAME" 26 | which systemctl &>/dev/null 27 | if [[ $? -eq 0 ]]; then 28 | systemctl daemon-reload 29 | systemctl restart $NAME 30 | else 31 | /etc/init.d/$NAME restart || true 32 | fi 33 | } 34 | 35 | function create_logdir { 36 | if [[ ! -d $LOG_DIR ]]; then 37 | mkdir -p $LOG_DIR 38 | chown -R $DAEMON_USER:$DAEMON_GROUP $LOG_DIR 39 | fi 40 | } 41 | 42 | # create user 43 | id $DAEMON_USER &>/dev/null 44 | if [[ $? -ne 0 ]]; then 45 | useradd --system -U -M $DAEMON_USER -s /bin/false -d /etc/$NAME 46 | fi 47 | 48 | create_logdir 49 | 50 | # set the configuration owner / permissions 51 | if [[ -f /etc/$NAME/$NAME.toml ]]; then 52 | chown -R $DAEMON_USER:$DAEMON_GROUP /etc/$NAME 53 | chmod 750 /etc/$NAME 54 | chmod 640 /etc/$NAME/$NAME.toml 55 | fi 56 | 57 | # show message on install 58 | if [[ $? -eq 0 ]]; then 59 | echo -e "\n\n\n" 60 | echo "---------------------------------------------------------------------------------" 61 | echo "The configuration file is located at:" 62 | echo " /etc/$NAME/$NAME.toml" 63 | echo "" 64 | echo "Some helpful commands for $NAME:" 65 | echo "" 66 | which systemctl &>/dev/null 67 | if [[ $? -eq 0 ]]; then 68 | echo "Start:" 69 | echo " $ sudo systemctl start $NAME" 70 | echo "" 71 | echo "Restart:" 72 | echo " $ sudo systemctl restart $NAME" 73 | echo "" 74 | echo "Stop:" 75 | echo " $ sudo systemctl stop $NAME" 76 | echo "" 77 | echo "Display logs:" 78 | echo " $ sudo journalctl -f -n 100 -u $NAME" 79 | else 80 | echo "Start:" 81 | echo " $ sudo /etc/init.d/$NAME start" 82 | echo "" 83 | echo "Restart:" 84 | echo " $ sudo /etc/init.d/$NAME restart" 85 | echo "" 86 | echo "Stop:" 87 | echo " $ sudo /etc/init.d/$NAME stop" 88 | echo "" 89 | echo "Display logs:" 90 | echo " $ sudo tail -f -n 100 $LOG_DIR" 91 | fi 92 | echo "---------------------------------------------------------------------------------" 93 | echo -e "\n\n\n" 94 | fi 95 | 96 | # add start script 97 | which systemctl &>/dev/null 98 | if [[ $? -eq 0 ]]; then 99 | install_systemd 100 | else 101 | install_init 102 | fi 103 | 104 | # restart on upgrade 105 | if [[ -n $2 ]]; then 106 | restart_service 107 | fi 108 | -------------------------------------------------------------------------------- /packaging/scripts/post-remove.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OLD_NAME=lora-gateway-bridge 4 | NAME=chirpstack-gateway-bridge 5 | 6 | function remove_systemd { 7 | systemctl stop $NAME 8 | systemctl disable $NAME 9 | rm -f /lib/systemd/system/$NAME.service 10 | } 11 | 12 | function remove_initd { 13 | /etc/init.d/$NAME stop 14 | update-rc.d -f $NAME remove 15 | rm -f /etc/init.d/$NAME 16 | rm -f /etc/init.d/$OLD_NAME 17 | } 18 | 19 | which systemctl &>/dev/null 20 | if [[ $? -eq 0 ]]; then 21 | remove_systemd 22 | else 23 | remove_initd 24 | fi 25 | -------------------------------------------------------------------------------- /packaging/scripts/pre-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | OLD_NAME=lora-gateway-bridge 4 | NAME=chirpstack-gateway-bridge 5 | 6 | # migrate config to new location 7 | if [[ -f /etc/$OLD_NAME/$OLD_NAME.toml ]] && [[ ! -h /etc/$OLD_NAME/$OLD_NAME.toml ]] && [[ ! -f /etc/$NAME/$NAME.toml ]]; then 8 | echo "Migrating /etc/$OLD_NAME/$OLD_NAME.toml to /etc/$NAME/$NAME.toml" 9 | 10 | mkdir -p /etc/$NAME 11 | mv /etc/$OLD_NAME/$OLD_NAME.toml /etc/$NAME/$NAME.toml 12 | 13 | echo "Creating symlink /etc/$OLD_NAME/$OLD_NAME.toml for backwards compatibility" 14 | ln -s /etc/$NAME/$NAME.toml /etc/$OLD_NAME/$OLD_NAME.toml 15 | fi 16 | 17 | function stop_init() { 18 | if [[ -f /etc/init.d/$OLD_NAME ]]; then 19 | echo "Stopping $OLD_NAME" 20 | /etc/init.d/$OLD_NAME stop 21 | fi 22 | } 23 | 24 | function stop_systemd() { 25 | if [[ -f /lib/systemd/system/$OLD_NAME.service ]]; then 26 | echo "Stopping $OLD_NAME" 27 | systemctl stop $OLD_NAME 28 | fi 29 | } 30 | 31 | # stop old service 32 | which systemctl &>/dev/null 33 | if [[ $? -eq 0 ]]; then 34 | stop_systemd 35 | else 36 | stop_init 37 | fi 38 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/MANIFEST: -------------------------------------------------------------------------------- 1 | manifest.version=1.0 2 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/VERSION_SDK: -------------------------------------------------------------------------------- 1 | 2.3 2 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml: -------------------------------------------------------------------------------- 1 | # This configuration provides a Semtech UDP packet-forwarder backend and 2 | # integrates with a MQTT broker. Many options and defaults have been omitted 3 | # for simplicity. 4 | # 5 | # See https://www.chirpstack.io/gateway-bridge/install/config/ for a full 6 | # configuration example and documentation. 7 | 8 | 9 | # Gateway backend configuration. 10 | [backend] 11 | # Backend type. 12 | type="semtech_udp" 13 | 14 | # Semtech UDP packet-forwarder backend. 15 | [backend.semtech_udp] 16 | 17 | # ip:port to bind the UDP listener to 18 | # 19 | # Example: 0.0.0.0:1700 to listen on port 1700 for all network interfaces. 20 | # This is the listener to which the packet-forwarder forwards its data 21 | # so make sure the 'serv_port_up' and 'serv_port_down' from your 22 | # packet-forwarder matches this port. 23 | udp_bind = "0.0.0.0:1700" 24 | 25 | 26 | # Integration configuration. 27 | [integration] 28 | # Payload marshaler. 29 | # 30 | # This defines how the MQTT payloads are encoded. Valid options are: 31 | # * protobuf: Protobuf encoding 32 | # * json: JSON encoding (easier for debugging, but less compact than 'protobuf') 33 | marshaler="protobuf" 34 | 35 | # MQTT integration configuration. 36 | [integration.mqtt] 37 | # Event topic template. 38 | event_topic_template="gateway/{{ .GatewayID }}/event/{{ .EventType }}" 39 | 40 | # Command topic template. 41 | command_topic_template="gateway/{{ .GatewayID }}/command/#" 42 | 43 | # MQTT authentication. 44 | [integration.mqtt.auth] 45 | # Type defines the MQTT authentication type to use. 46 | # 47 | # Set this to the name of one of the sections below. 48 | type="generic" 49 | 50 | # Generic MQTT authentication. 51 | [integration.mqtt.auth.generic] 52 | # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) 53 | server="tcp://127.0.0.1:1883" 54 | 55 | # Connect with the given username (optional) 56 | username="" 57 | 58 | # Connect with the given password (optional) 59 | password="" 60 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/etc/init.d/chirpstack-gateway-bridge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | start() { 4 | echo "Starting chirpstack-gateway-bridge" 5 | start-stop-daemon \ 6 | --start \ 7 | --background \ 8 | --make-pidfile \ 9 | --pidfile /var/run/chirpstack-gateway-bridge.pid \ 10 | --exec /tmp/mdm/pktfwd/firmware/opt/chirpstack-gateway-bridge/chirpstack-gateway-bridge -- -c /etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml 11 | } 12 | 13 | stop() { 14 | echo "Stopping chirpstack-gateway-bridge" 15 | start-stop-daemon \ 16 | --stop \ 17 | --oknodo \ 18 | --quiet \ 19 | --pidfile /var/run/chirpstack-gateway-bridge.pid 20 | } 21 | 22 | restart() { 23 | stop 24 | sleep 1 25 | start 26 | } 27 | 28 | case "$1" in 29 | start) 30 | start 31 | ;; 32 | stop) 33 | stop 34 | ;; 35 | restart|reload) 36 | restart 37 | ;; 38 | *) 39 | echo "Usage: $0 {start|stop|restart}" 40 | exit 1 41 | esac 42 | 43 | exit $? 44 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/etc/init.d/lora-packet-forwarder: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | start() { 4 | echo "Starting lora-packet-forwarder" 5 | start-stop-daemon \ 6 | --start \ 7 | --background \ 8 | --make-pidfile \ 9 | --pidfile /var/run/lora-packet-forwarder.pid \ 10 | --exec /tools/pkt_forwarder -- -c /etc/lora-packet-forwarder/config.json -g /dev/ttyS1 11 | } 12 | 13 | stop() { 14 | echo "Stopping lora-packet-forwarder" 15 | start-stop-daemon \ 16 | --stop \ 17 | --oknodo \ 18 | --quiet \ 19 | --pidfile /var/run/lora-packet-forwarder.pid 20 | } 21 | 22 | restart() { 23 | stop 24 | sleep 1 25 | start 26 | } 27 | 28 | case "$1" in 29 | start) 30 | start 31 | ;; 32 | stop) 33 | stop 34 | ;; 35 | restart|reload) 36 | restart 37 | ;; 38 | *) 39 | echo "Usage: $0 {start|stop|restart}" 40 | exit 1 41 | esac 42 | 43 | exit $? 44 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/POSTINSTALL: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running post-install script" 4 | 5 | if [ ! -L /etc/init.d/lora-packet-forwarder ]; then 6 | echo "Install lora-packet-forwarder init script" 7 | ln -s /tmp/mdm/pktfwd/firmware/etc/init.d/lora-packet-forwarder /etc/init.d/lora-packet-forwarder 8 | fi 9 | 10 | if [ ! -L /etc/init.d/chirpstack-gateway-bridge ]; then 11 | echo "Install chirpstack-gateway-bridge init script" 12 | ln -s /tmp/mdm/pktfwd/firmware/etc/init.d/chirpstack-gateway-bridge /etc/init.d/chirpstack-gateway-bridge 13 | fi 14 | 15 | if [ ! -d /etc/chirpstack-gateway-bridge ]; then 16 | mkdir /etc/chirpstack-gateway-bridge 17 | fi 18 | 19 | if [ ! -d /etc/lora-packet-forwarder ]; then 20 | mkdir /etc/lora-packet-forwarder 21 | fi 22 | 23 | if [ ! -f /etc/lora-packet-forwarder/config.json ]; then 24 | cp /tmp/mdm/pktfwd/firmware/etc/lora-packet-forwarder/config.json /etc/lora-packet-forwarder/config.json 25 | 26 | GWID_MIDFIX="FFFE" 27 | GWID_BEGIN=$(ip link show eth0 | awk '/ether/ {print $2}' | awk -F\: '{print $1$2$3}') 28 | GWID_END=$(ip link show eth0 | awk '/ether/ {print $2}' | awk -F\: '{print $4$5$6}') 29 | sed -i 's/\(^\s*"gateway_ID":\s*"\).\{16\}"\s*\(,\?\).*$/\1'${GWID_BEGIN}${GWID_MIDFIX}${GWID_END}'"\2/' /etc/lora-packet-forwarder/config.json 30 | fi 31 | 32 | if [ ! -f /etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml ]; then 33 | cp /tmp/mdm/pktfwd/firmware/etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml /etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml 34 | fi 35 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/POSTUNINSTALL: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # remove symlinks 4 | rm /etc/init.d/lora-packet-forwarder 5 | rm /etc/init.d/chirpstack-gateway-bridge 6 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/PREUNINSTALL: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running pre-uninstall script" 4 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/RESTART: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /etc/init.d/lora-packet-forwarder restart 6 | /etc/init.d/chirpstack-gateway-bridge restart 7 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/START: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /etc/init.d/lora-packet-forwarder start 6 | /etc/init.d/chirpstack-gateway-bridge start 7 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/STATUS: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running status script" 4 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/STOP: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | /etc/init.d/lora-packet-forwarder stop 6 | /etc/init.d/chirpstack-gateway-bridge stop 7 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/files/scripts/VERSION: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /tmp/mdm/pktfwd/firmware/opt/chirpstack-gateway-bridge/chirpstack-gateway-bridge version 4 | -------------------------------------------------------------------------------- /packaging/vendor/cisco/IXM-LPWA/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | PACKAGE_NAME="chirpstack-gateway-bridge" 6 | PACKAGE_VERSION=$1 7 | REV="r1" 8 | 9 | PACKAGE_URL="https://artifacts.chirpstack.io/downloads/chirpstack-gateway-bridge/chirpstack-gateway-bridge_${PACKAGE_VERSION}_linux_armv5.tar.gz" 10 | DIR=`dirname $0` 11 | FILES_DIR="${DIR}/files" 12 | KEY_DIR="${DIR}/key" 13 | PACKAGE_DIR="${DIR}/package" 14 | TMP_DIR="${DIR}/temp" 15 | 16 | # Cleanup 17 | rm -rf $PACKAGE_DIR 18 | rm -rf $TMP_DIR 19 | 20 | if [ ! -d $KEY_DIR ]; then 21 | echo "Key-pair does not yet exist, creating one." 22 | mkdir -p $KEY_DIR 23 | openssl genrsa -out $KEY_DIR/private.key 2048 24 | openssl rsa -pubout -in $KEY_DIR/private.key > $KEY_DIR/public.key 25 | fi 26 | 27 | mkdir -p $PACKAGE_DIR 28 | mkdir -p $TMP_DIR/package 29 | 30 | # Copy package files 31 | cp -R $FILES_DIR/* $PACKAGE_DIR 32 | 33 | # ChirpStack Gateway Bridge binary 34 | mkdir -p $PACKAGE_DIR/opt/$PACKAGE_NAME 35 | wget -P $PACKAGE_DIR/opt/$PACKAGE_NAME $PACKAGE_URL 36 | tar zxf $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz -C $PACKAGE_DIR/opt/$PACKAGE_NAME 37 | rm $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz 38 | 39 | echo "Tarring" 40 | tar cvfz $TMP_DIR/files.pkg.tar.gz -C $PACKAGE_DIR . 41 | 42 | echo "Create MD5 check sum file" 43 | cd $TMP_DIR 44 | md5sum files.pkg.tar.gz > cpkg.hash 45 | 46 | echo "Packaging files and cpkg.hash file" 47 | tar cvf files.hashedpkg.tar.gz files.pkg.tar.gz cpkg.hash 48 | cd .. 49 | 50 | echo "Signing" 51 | openssl dgst -sha1 -sign $KEY_DIR/private.key -out $TMP_DIR/files.sig $TMP_DIR/files.pkg.tar.gz 52 | 53 | 54 | cat $TMP_DIR/files.pkg.tar.gz $TMP_DIR/files.sig > $TMP_DIR/package/${PACKAGE_NAME}_${PACKAGE_VERSION}_${REV}.cpkg 55 | cp $KEY_DIR/public.key $TMP_DIR/package/ 56 | 57 | tar cvfz ${PACKAGE_NAME}_${PACKAGE_VERSION}_${REV}.tar.gz -C $TMP_DIR/package . 58 | -------------------------------------------------------------------------------- /packaging/vendor/dragino/LG308/files/chirpstack-gateway-bridge.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=100 4 | STOP=100 5 | 6 | NAME="chirpstack-gateway-bridge" 7 | DAEMON_BIN=/opt/$NAME/$NAME 8 | DAEMON_CONF=/etc/$NAME/$NAME.toml 9 | DAEMON_PID=/var/run/$NAME.pid 10 | 11 | start() { 12 | echo "Starting $NAME" 13 | start-stop-daemon \ 14 | -S \ 15 | -b \ 16 | -m \ 17 | -p $DAEMON_PID \ 18 | -x $DAEMON_BIN -- --config $DAEMON_CONF 19 | } 20 | 21 | stop() { 22 | echo "Stopping $NAME" 23 | start-stop-daemon \ 24 | -K \ 25 | -p $DAEMON_PID 26 | } 27 | -------------------------------------------------------------------------------- /packaging/vendor/dragino/LG308/files/chirpstack-gateway-bridge.toml: -------------------------------------------------------------------------------- 1 | # This configuration provides a Semtech UDP packet-forwarder backend and 2 | # integrates with a MQTT broker. Many options and defaults have been omitted 3 | # for simplicity. 4 | # 5 | # See https://www.chirpstack.io/gateway-bridge/install/config/ for a full 6 | # configuration example and documentation. 7 | 8 | 9 | [general] 10 | # debug=5, info=4, warning=3, error=2, fatal=1, panic=0 11 | log_level=4 12 | 13 | # Log to syslog. 14 | # 15 | # When set to true, log messages are being written to syslog. 16 | log_to_syslog=true 17 | 18 | 19 | # Gateway backend configuration. 20 | [backend] 21 | # Backend type. 22 | type="semtech_udp" 23 | 24 | # Semtech UDP packet-forwarder backend. 25 | [backend.semtech_udp] 26 | 27 | # ip:port to bind the UDP listener to 28 | # 29 | # Example: 0.0.0.0:1700 to listen on port 1700 for all network interfaces. 30 | # This is the listener to which the packet-forwarder forwards its data 31 | # so make sure the 'serv_port_up' and 'serv_port_down' from your 32 | # packet-forwarder matches this port. 33 | udp_bind = "0.0.0.0:1700" 34 | 35 | 36 | # Integration configuration. 37 | [integration] 38 | # Payload marshaler. 39 | # 40 | # This defines how the MQTT payloads are encoded. Valid options are: 41 | # * protobuf: Protobuf encoding 42 | # * json: JSON encoding (easier for debugging, but less compact than 'protobuf') 43 | marshaler="protobuf" 44 | 45 | # MQTT integration configuration. 46 | [integration.mqtt] 47 | # Event topic template. 48 | event_topic_template="gateway/{{ .GatewayID }}/event/{{ .EventType }}" 49 | 50 | # Command topic template. 51 | command_topic_template="gateway/{{ .GatewayID }}/command/#" 52 | 53 | # MQTT authentication. 54 | [integration.mqtt.auth] 55 | # Type defines the MQTT authentication type to use. 56 | # 57 | # Set this to the name of one of the sections below. 58 | type="generic" 59 | 60 | # Generic MQTT authentication. 61 | [integration.mqtt.auth.generic] 62 | # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) 63 | server="tcp://127.0.0.1:1883" 64 | 65 | # Connect with the given username (optional) 66 | username="" 67 | 68 | # Connect with the given password (optional) 69 | password="" 70 | -------------------------------------------------------------------------------- /packaging/vendor/dragino/LG308/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="chirpstack-gateway-bridge" 4 | PACKAGE_VERSION=$1 5 | REV="r1" 6 | 7 | 8 | PACKAGE_URL="https://artifacts.chirpstack.io/downloads/chirpstack-gateway-bridge/chirpstack-gateway-bridge_${PACKAGE_VERSION}_linux_mips.tar.gz" 9 | DIR=`dirname $0` 10 | PACKAGE_DIR="${DIR}/package" 11 | 12 | # Cleanup 13 | rm -rf $PACKAGE_DIR 14 | 15 | # CONTROL 16 | mkdir -p $PACKAGE_DIR/CONTROL 17 | cat > $PACKAGE_DIR/CONTROL/control << EOF 18 | Package: $PACKAGE_NAME 19 | Version: $PACKAGE_VERSION-$REV 20 | Architecture: mips_24kc 21 | Maintainer: Orne Brocaar 22 | Priority: optional 23 | Section: network 24 | Source: N/A 25 | Description: ChirpStack Gateway Bridge 26 | EOF 27 | 28 | cat > $PACKAGE_DIR/CONTROL/postinst << EOF 29 | #!/bin/sh 30 | /etc/init.d/chirpstack-gateway-bridge enable 31 | EOF 32 | chmod 755 $PACKAGE_DIR/CONTROL/postinst 33 | 34 | cat > $PACKAGE_DIR/CONTROL/conffiles << EOF 35 | /etc/$PACKAGE_NAME/$PACKAGE_NAME.toml 36 | EOF 37 | 38 | # Files 39 | mkdir -p $PACKAGE_DIR/opt/$PACKAGE_NAME 40 | mkdir -p $PACKAGE_DIR/etc/$PACKAGE_NAME 41 | mkdir -p $PACKAGE_DIR/etc/init.d 42 | 43 | cp files/$PACKAGE_NAME.toml $PACKAGE_DIR/etc/$PACKAGE_NAME/$PACKAGE_NAME.toml 44 | cp files/$PACKAGE_NAME.init $PACKAGE_DIR/etc/init.d/$PACKAGE_NAME 45 | wget -P $PACKAGE_DIR/opt/$PACKAGE_NAME $PACKAGE_URL 46 | tar zxf $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz -C $PACKAGE_DIR/opt/$PACKAGE_NAME 47 | rm $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz 48 | 49 | # Package 50 | opkg-build -c -o root -g root $PACKAGE_DIR 51 | 52 | # Cleanup 53 | rm -rf $PACKAGE_DIR 54 | -------------------------------------------------------------------------------- /packaging/vendor/kerlink/keros-gws/files/chirpstack-gateway-bridge.init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NAME="chirpstack-gateway-bridge" 4 | DESC="ChirpStack Gateway Bridge" 5 | DAEMON_BIN=/opt/$NAME/$NAME 6 | DAEMON_CONF=/etc/$NAME/$NAME.toml 7 | DAEMON_PID=/var/run/$NAME.pid 8 | 9 | function iptables_accept { 10 | [ -n "${1}" ] || exit 1 11 | local RULE="OUTPUT -t filter -p tcp --dport ${1} -j ACCEPT" 12 | iptables -C ${RULE} 2> /dev/null || iptables -I ${RULE} 13 | local RULE="INPUT -t filter -p tcp --sport ${1} -m conntrack --ctstate ESTABLISHED -j ACCEPT" 14 | iptables -C ${RULE} 2> /dev/null || iptables -I ${RULE} 15 | } 16 | 17 | function do_start { 18 | echo "Starting $NAME" 19 | iptables_accept 1883 20 | iptables_accept 8883 21 | 22 | start-stop-daemon \ 23 | --start \ 24 | --background \ 25 | --make-pidfile \ 26 | --pidfile $DAEMON_PID \ 27 | --exec $DAEMON_BIN -- --config $DAEMON_CONF 28 | } 29 | 30 | function do_stop { 31 | echo "Stopping $NAME" 32 | start-stop-daemon \ 33 | --stop \ 34 | --oknodo \ 35 | --quiet \ 36 | --pidfile $DAEMON_PID 37 | } 38 | 39 | case "$1" in 40 | "start") 41 | do_start 42 | ;; 43 | "stop") 44 | do_stop 45 | ;; 46 | "restart") 47 | do_stop 48 | do_start 49 | ;; 50 | *) 51 | echo "Usage: $1 {start|stop|restart}" 52 | exit 1 53 | ;; 54 | esac 55 | -------------------------------------------------------------------------------- /packaging/vendor/kerlink/keros-gws/files/chirpstack-gateway-bridge.monit: -------------------------------------------------------------------------------- 1 | check process chirpstack-gateway-bridge pidfile /var/run/chirpstack-gateway-bridge.pid 2 | start program = "/etc/init.d/chirpstack-gateway-bridge start" 3 | stop program = "/etc/init.d/chirpstack-gateway-bridge stop" 4 | -------------------------------------------------------------------------------- /packaging/vendor/kerlink/keros-gws/files/chirpstack-gateway-bridge.toml: -------------------------------------------------------------------------------- 1 | # This configuration provides a Semtech UDP packet-forwarder backend and 2 | # integrates with a MQTT broker. Many options and defaults have been omitted 3 | # for simplicity. 4 | # 5 | # See https://www.chirpstack.io/gateway-bridge/install/config/ for a full 6 | # configuration example and documentation. 7 | 8 | 9 | [general] 10 | # debug=5, info=4, warning=3, error=2, fatal=1, panic=0 11 | log_level=4 12 | 13 | # Log to syslog. 14 | # 15 | # When set to true, log messages are being written to syslog. 16 | log_to_syslog=true 17 | 18 | 19 | # Gateway backend configuration. 20 | [backend] 21 | # Backend type. 22 | type="semtech_udp" 23 | 24 | # Semtech UDP packet-forwarder backend. 25 | [backend.semtech_udp] 26 | 27 | # ip:port to bind the UDP listener to 28 | # 29 | # Example: 0.0.0.0:1700 to listen on port 1700 for all network interfaces. 30 | # This is the listener to which the packet-forwarder forwards its data 31 | # so make sure the 'serv_port_up' and 'serv_port_down' from your 32 | # packet-forwarder matches this port. 33 | udp_bind = "0.0.0.0:1700" 34 | 35 | 36 | # Integration configuration. 37 | [integration] 38 | # Payload marshaler. 39 | # 40 | # This defines how the MQTT payloads are encoded. Valid options are: 41 | # * protobuf: Protobuf encoding 42 | # * json: JSON encoding (easier for debugging, but less compact than 'protobuf') 43 | marshaler="protobuf" 44 | 45 | # MQTT integration configuration. 46 | [integration.mqtt] 47 | # Event topic template. 48 | event_topic_template="gateway/{{ .GatewayID }}/event/{{ .EventType }}" 49 | 50 | # Command topic template. 51 | command_topic_template="gateway/{{ .GatewayID }}/command/#" 52 | 53 | # MQTT authentication. 54 | [integration.mqtt.auth] 55 | # Type defines the MQTT authentication type to use. 56 | # 57 | # Set this to the name of one of the sections below. 58 | type="generic" 59 | 60 | # Generic MQTT authentication. 61 | [integration.mqtt.auth.generic] 62 | # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) 63 | server="tcp://127.0.0.1:1883" 64 | 65 | # Connect with the given username (optional) 66 | username="" 67 | 68 | # Connect with the given password (optional) 69 | password="" 70 | -------------------------------------------------------------------------------- /packaging/vendor/kerlink/keros-gws/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="chirpstack-gateway-bridge" 4 | PACKAGE_VERSION=$1 5 | REV="r1" 6 | 7 | 8 | PACKAGE_URL="https://artifacts.chirpstack.io/downloads/chirpstack-gateway-bridge/chirpstack-gateway-bridge_${PACKAGE_VERSION}_linux_armv7.tar.gz" 9 | DIR=`dirname $0` 10 | PACKAGE_DIR="${DIR}/package" 11 | 12 | # Cleanup 13 | rm -rf $PACKAGE_DIR 14 | 15 | # CONTROL 16 | mkdir -p $PACKAGE_DIR/CONTROL 17 | cat > $PACKAGE_DIR/CONTROL/control << EOF 18 | Package: $PACKAGE_NAME 19 | Version: $PACKAGE_VERSION-$REV 20 | Architecture: klkgw 21 | Maintainer: Orne Brocaar 22 | Priority: optional 23 | Section: network 24 | Source: N/A 25 | Description: ChirpStack Gateway Bridge 26 | EOF 27 | 28 | cat > $PACKAGE_DIR/CONTROL/conffiles << EOF 29 | /etc/$PACKAGE_NAME/$PACKAGE_NAME.toml 30 | EOF 31 | 32 | # Files 33 | mkdir -p $PACKAGE_DIR/opt/$PACKAGE_NAME 34 | mkdir -p $PACKAGE_DIR/etc/$PACKAGE_NAME 35 | mkdir -p $PACKAGE_DIR/etc/monit.d 36 | mkdir -p $PACKAGE_DIR/etc/init.d 37 | 38 | cp files/$PACKAGE_NAME.toml $PACKAGE_DIR/etc/$PACKAGE_NAME/$PACKAGE_NAME.toml 39 | cp files/$PACKAGE_NAME.monit $PACKAGE_DIR/etc/monit.d/$PACKAGE_NAME 40 | cp files/$PACKAGE_NAME.init $PACKAGE_DIR/etc/init.d/$PACKAGE_NAME 41 | wget -P $PACKAGE_DIR/opt/$PACKAGE_NAME $PACKAGE_URL 42 | tar zxf $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz -C $PACKAGE_DIR/opt/$PACKAGE_NAME 43 | rm $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz 44 | 45 | # Package 46 | opkg-build -o root -g root $PACKAGE_DIR 47 | 48 | # Cleanup 49 | rm -rf $PACKAGE_DIR 50 | -------------------------------------------------------------------------------- /packaging/vendor/multitech/conduit/files/chirpstack-gateway-bridge.init: -------------------------------------------------------------------------------- 1 | NAME="chirpstack-gateway-bridge" 2 | DESC="ChirpStack Gateway Bridge" 3 | DAEMON_BIN=/opt/$NAME/$NAME 4 | DAEMON_CONF=/etc/$NAME 5 | DAEMON_PID=/var/run/$NAME.pid 6 | 7 | function do_start { 8 | echo "Starting $NAME" 9 | start-stop-daemon \ 10 | --start \ 11 | --background \ 12 | --make-pidfile \ 13 | --pidfile $DAEMON_PID \ 14 | --exec $DAEMON_BIN -- --config /var/config/$NAME/$NAME.toml 15 | } 16 | 17 | function do_stop { 18 | echo "Stopping $NAME" 19 | start-stop-daemon \ 20 | --stop \ 21 | --oknodo \ 22 | --quiet \ 23 | --pidfile $DAEMON_PID 24 | } 25 | 26 | case "$1" in 27 | "start") 28 | do_start 29 | ;; 30 | "stop") 31 | do_stop 32 | ;; 33 | "restart") 34 | do_stop 35 | do_start 36 | ;; 37 | *) 38 | echo "Usage: $1 {start|stop|restart}" 39 | exit 1 40 | ;; 41 | esac 42 | 43 | -------------------------------------------------------------------------------- /packaging/vendor/multitech/conduit/files/chirpstack-gateway-bridge.monit: -------------------------------------------------------------------------------- 1 | check process chirpstack-gateway-bridge pidfile /var/run/chirpstack-gateway-bridge.pid 2 | start program = "/bin/bash -c '/etc/init.d/chirpstack-gateway-bridge start'" 3 | stop program = "/bin/bash -c '/etc/init.d/chirpstack-gateway-bridge stop'" 4 | 5 | -------------------------------------------------------------------------------- /packaging/vendor/multitech/conduit/files/chirpstack-gateway-bridge.toml: -------------------------------------------------------------------------------- 1 | # This configuration provides a Semtech UDP packet-forwarder backend and 2 | # integrates with a MQTT broker. Many options and defaults have been omitted 3 | # for simplicity. 4 | # 5 | # See https://www.chirpstack.io/gateway-bridge/install/config/ for a full 6 | # configuration example and documentation. 7 | 8 | 9 | [general] 10 | # debug=5, info=4, warning=3, error=2, fatal=1, panic=0 11 | log_level=4 12 | 13 | # Log to syslog. 14 | # 15 | # When set to true, log messages are being written to syslog. 16 | log_to_syslog=true 17 | 18 | 19 | # Gateway backend configuration. 20 | [backend] 21 | # Backend type. 22 | type="semtech_udp" 23 | 24 | # Semtech UDP packet-forwarder backend. 25 | [backend.semtech_udp] 26 | 27 | # ip:port to bind the UDP listener to 28 | # 29 | # Example: 0.0.0.0:1700 to listen on port 1700 for all network interfaces. 30 | # This is the listener to which the packet-forwarder forwards its data 31 | # so make sure the 'serv_port_up' and 'serv_port_down' from your 32 | # packet-forwarder matches this port. 33 | udp_bind = "0.0.0.0:1700" 34 | 35 | 36 | # Integration configuration. 37 | [integration] 38 | # Payload marshaler. 39 | # 40 | # This defines how the MQTT payloads are encoded. Valid options are: 41 | # * protobuf: Protobuf encoding 42 | # * json: JSON encoding (easier for debugging, but less compact than 'protobuf') 43 | marshaler="protobuf" 44 | 45 | # MQTT integration configuration. 46 | [integration.mqtt] 47 | # Event topic template. 48 | event_topic_template="gateway/{{ .GatewayID }}/event/{{ .EventType }}" 49 | 50 | # Command topic template. 51 | command_topic_template="gateway/{{ .GatewayID }}/command/#" 52 | 53 | # MQTT authentication. 54 | [integration.mqtt.auth] 55 | # Type defines the MQTT authentication type to use. 56 | # 57 | # Set this to the name of one of the sections below. 58 | type="generic" 59 | 60 | # Generic MQTT authentication. 61 | [integration.mqtt.auth.generic] 62 | # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) 63 | server="tcp://127.0.0.1:1883" 64 | 65 | # Connect with the given username (optional) 66 | username="" 67 | 68 | # Connect with the given password (optional) 69 | password="" 70 | 71 | -------------------------------------------------------------------------------- /packaging/vendor/multitech/conduit/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="chirpstack-gateway-bridge" 4 | PACKAGE_VERSION=$1 5 | REV="r1" 6 | 7 | 8 | PACKAGE_URL="https://artifacts.chirpstack.io/downloads/chirpstack-gateway-bridge/chirpstack-gateway-bridge_${PACKAGE_VERSION}_linux_armv5.tar.gz" 9 | DIR=`dirname $0` 10 | PACKAGE_DIR="${DIR}/package" 11 | 12 | # Cleanup 13 | rm -rf $PACKAGE_DIR 14 | 15 | # CONTROL 16 | mkdir -p $PACKAGE_DIR/CONTROL 17 | cat > $PACKAGE_DIR/CONTROL/control << EOF 18 | Package: $PACKAGE_NAME 19 | Version: $PACKAGE_VERSION-$REV 20 | Architecture: arm926ejste 21 | Maintainer: Orne Brocaar 22 | Priority: optional 23 | Section: network 24 | Source: N/A 25 | Description: ChirpStack Gateway Bridge 26 | EOF 27 | 28 | cat > $PACKAGE_DIR/CONTROL/postinst << EOF 29 | sed -i "s/ENABLED=.*/ENABLED=\"yes\"/" /etc/default/monit 30 | update-rc.d monit defaults 31 | /etc/init.d/monit start 32 | update-rc.d chirpstack-gateway-bridge defaults 33 | /usr/bin/monit reload 34 | EOF 35 | chmod 755 $PACKAGE_DIR/CONTROL/postinst 36 | 37 | cat > $PACKAGE_DIR/CONTROL/conffiles << EOF 38 | /var/config/$PACKAGE_NAME/$PACKAGE_NAME.toml 39 | EOF 40 | 41 | # Files 42 | mkdir -p $PACKAGE_DIR/opt/$PACKAGE_NAME 43 | mkdir -p $PACKAGE_DIR/var/config/$PACKAGE_NAME 44 | mkdir -p $PACKAGE_DIR/etc/monit.d 45 | mkdir -p $PACKAGE_DIR/etc/init.d 46 | 47 | cp files/$PACKAGE_NAME.toml $PACKAGE_DIR/var/config/$PACKAGE_NAME/$PACKAGE_NAME.toml 48 | cp files/$PACKAGE_NAME.monit $PACKAGE_DIR/etc/monit.d/$PACKAGE_NAME 49 | cp files/$PACKAGE_NAME.init $PACKAGE_DIR/etc/init.d/$PACKAGE_NAME 50 | wget -P $PACKAGE_DIR/opt/$PACKAGE_NAME $PACKAGE_URL 51 | tar zxf $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz -C $PACKAGE_DIR/opt/$PACKAGE_NAME 52 | rm $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz 53 | 54 | # Package 55 | opkg-build -o root -g root $PACKAGE_DIR 56 | 57 | # Cleanup 58 | rm -rf $PACKAGE_DIR 59 | -------------------------------------------------------------------------------- /packaging/vendor/tektelic/kona/files/chirpstack-gateway-bridge.init: -------------------------------------------------------------------------------- 1 | NAME="chirpstack-gateway-bridge" 2 | DESC="ChirpStack Gateway Bridge" 3 | DAEMON_BIN=/opt/$NAME/$NAME 4 | DAEMON_CONF=/etc/$NAME 5 | DAEMON_PID=/var/run/$NAME.pid 6 | 7 | function do_start { 8 | echo "Starting $NAME" 9 | start-stop-daemon \ 10 | --start \ 11 | --background \ 12 | --make-pidfile \ 13 | --pidfile $DAEMON_PID \ 14 | --exec $DAEMON_BIN 15 | } 16 | 17 | function do_stop { 18 | echo "Stopping $NAME" 19 | start-stop-daemon \ 20 | --stop \ 21 | --oknodo \ 22 | --quiet \ 23 | --pidfile $DAEMON_PID 24 | } 25 | 26 | case "$1" in 27 | "start") 28 | do_start 29 | ;; 30 | "stop") 31 | do_stop 32 | ;; 33 | "restart") 34 | do_stop 35 | do_start 36 | ;; 37 | *) 38 | echo "Usage: $1 {start|stop|restart}" 39 | exit 1 40 | ;; 41 | esac 42 | 43 | -------------------------------------------------------------------------------- /packaging/vendor/tektelic/kona/files/chirpstack-gateway-bridge.monit: -------------------------------------------------------------------------------- 1 | check process chirpstack-gateway-bridge pidfile /var/run/chirpstack-gateway-bridge.pid 2 | start program = "/bin/bash -c '/etc/init.d/chirpstack-gateway-bridge start'" 3 | stop program = "/bin/bash -c '/etc/init.d/chirpstack-gateway-bridge stop'" 4 | 5 | -------------------------------------------------------------------------------- /packaging/vendor/tektelic/kona/files/chirpstack-gateway-bridge.toml: -------------------------------------------------------------------------------- 1 | # This configuration provides a Semtech UDP packet-forwarder backend and 2 | # integrates with a MQTT broker. Many options and defaults have been omitted 3 | # for simplicity. 4 | # 5 | # See https://www.chirpstack.io/gateway-bridge/install/config/ for a full 6 | # configuration example and documentation. 7 | 8 | 9 | [general] 10 | # debug=5, info=4, warning=3, error=2, fatal=1, panic=0 11 | log_level=4 12 | 13 | # Log to syslog. 14 | # 15 | # When set to true, log messages are being written to syslog. 16 | log_to_syslog=true 17 | 18 | 19 | # Gateway backend configuration. 20 | [backend] 21 | # Backend type. 22 | type="semtech_udp" 23 | 24 | # Semtech UDP packet-forwarder backend. 25 | [backend.semtech_udp] 26 | 27 | # ip:port to bind the UDP listener to 28 | # 29 | # Example: 0.0.0.0:1700 to listen on port 1700 for all network interfaces. 30 | # This is the listener to which the packet-forwarder forwards its data 31 | # so make sure the 'serv_port_up' and 'serv_port_down' from your 32 | # packet-forwarder matches this port. 33 | udp_bind = "0.0.0.0:1700" 34 | 35 | 36 | # Integration configuration. 37 | [integration] 38 | # Payload marshaler. 39 | # 40 | # This defines how the MQTT payloads are encoded. Valid options are: 41 | # * protobuf: Protobuf encoding 42 | # * json: JSON encoding (easier for debugging, but less compact than 'protobuf') 43 | marshaler="protobuf" 44 | 45 | # MQTT integration configuration. 46 | [integration.mqtt] 47 | # Event topic template. 48 | event_topic_template="gateway/{{ .GatewayID }}/event/{{ .EventType }}" 49 | 50 | # Command topic template. 51 | command_topic_template="gateway/{{ .GatewayID }}/command/#" 52 | 53 | # MQTT authentication. 54 | [integration.mqtt.auth] 55 | # Type defines the MQTT authentication type to use. 56 | # 57 | # Set this to the name of one of the sections below. 58 | type="generic" 59 | 60 | # Generic MQTT authentication. 61 | [integration.mqtt.auth.generic] 62 | # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) 63 | server="tcp://127.0.0.1:1883" 64 | 65 | # Connect with the given username (optional) 66 | username="" 67 | 68 | # Connect with the given password (optional) 69 | password="" 70 | 71 | -------------------------------------------------------------------------------- /packaging/vendor/tektelic/kona/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="chirpstack-gateway-bridge" 4 | PACKAGE_VERSION=$1 5 | REV="r1" 6 | 7 | 8 | PACKAGE_URL="https://artifacts.chirpstack.io/downloads/chirpstack-gateway-bridge/chirpstack-gateway-bridge_${PACKAGE_VERSION}_linux_armv5.tar.gz" 9 | DIR=`dirname $0` 10 | PACKAGE_DIR="${DIR}/package" 11 | 12 | # Cleanup 13 | rm -rf $PACKAGE_DIR 14 | 15 | # CONTROL 16 | mkdir -p $PACKAGE_DIR/CONTROL 17 | cat > $PACKAGE_DIR/CONTROL/control << EOF 18 | Package: $PACKAGE_NAME 19 | Version: $PACKAGE_VERSION-$REV 20 | Architecture: kona 21 | Maintainer: Orne Brocaar 22 | Priority: optional 23 | Section: network 24 | Source: N/A 25 | Description: ChirpStack Gateway Bridge 26 | EOF 27 | 28 | cat > $PACKAGE_DIR/CONTROL/postinst << EOF 29 | /usr/bin/monit reload 30 | EOF 31 | chmod 755 $PACKAGE_DIR/CONTROL/postinst 32 | 33 | cat > $PACKAGE_DIR/CONTROL/conffiles << EOF 34 | /etc/$PACKAGE_NAME/$PACKAGE_NAME.toml 35 | EOF 36 | 37 | # Files 38 | mkdir -p $PACKAGE_DIR/opt/$PACKAGE_NAME 39 | mkdir -p $PACKAGE_DIR/etc/$PACKAGE_NAME 40 | mkdir -p $PACKAGE_DIR/etc/monit.d 41 | mkdir -p $PACKAGE_DIR/etc/init.d 42 | 43 | cp files/$PACKAGE_NAME.toml $PACKAGE_DIR/etc/$PACKAGE_NAME/$PACKAGE_NAME.toml 44 | cp files/$PACKAGE_NAME.monit $PACKAGE_DIR/etc/monit.d/$PACKAGE_NAME 45 | cp files/$PACKAGE_NAME.init $PACKAGE_DIR/etc/init.d/$PACKAGE_NAME 46 | wget -P $PACKAGE_DIR/opt/$PACKAGE_NAME $PACKAGE_URL 47 | tar zxf $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz -C $PACKAGE_DIR/opt/$PACKAGE_NAME 48 | rm $PACKAGE_DIR/opt/$PACKAGE_NAME/*.tar.gz 49 | 50 | # Package 51 | opkg-build -o root -g root $PACKAGE_DIR 52 | 53 | # Cleanup 54 | rm -rf $PACKAGE_DIR 55 | --------------------------------------------------------------------------------