├── .gitignore ├── Dockerfile-devel ├── LICENSE ├── Makefile ├── README.md ├── cmd └── chirpstack-simulator │ ├── cmd │ ├── configfile.go │ ├── root.go │ ├── root_run.go │ └── version.go │ └── main.go ├── docker-compose.yml ├── examples └── single_uplink │ └── main.go ├── go.mod ├── go.sum ├── internal ├── as │ └── api.go ├── config │ └── config.go ├── ns │ └── mqtt.go └── simulator │ ├── metrics.go │ ├── session_keys.go │ └── simulator.go └── simulator ├── device.go ├── gateway.go ├── metrics.go └── session_keys.go /.gitignore: -------------------------------------------------------------------------------- 1 | # hidden files 2 | .* 3 | 4 | # config 5 | /*.toml 6 | /*.json 7 | 8 | # builds 9 | /build/ 10 | -------------------------------------------------------------------------------- /Dockerfile-devel: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine 2 | 3 | ENV PROJECT_PATH=/chirpstack-simulator 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 tzdata make git bash 9 | 10 | RUN mkdir -p $PROJECT_PATH 11 | RUN git config --global --add safe.directory $PROJECT_PATH 12 | COPY . $PROJECT_PATH 13 | WORKDIR $PROJECT_PATH 14 | -------------------------------------------------------------------------------- /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 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-simulator cmd/chirpstack-simulator/main.go 8 | 9 | clean: 10 | @echo "Cleaning up workspace" 11 | @rm -rf build 12 | @rm -rf dist 13 | @rm -rf docs/public 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChirpStack Simulator 2 | 3 | ChirpStack Simulator is an open-source simulator for the [ChirpStack](https://www.chirpstack.io) 4 | open-source LoRaWAN® Network-Server (v4). It simulates 5 | a configurable number of devices and gateways, which will be automatically 6 | created on starting the simulation. 7 | 8 | This project has been developed together with [TWTG](https://www.twtg.io/). 9 | 10 | ## Building 11 | 12 | The recommended way to compile the simulator code is using [Docker Compose](https://docs.docker.com/compose/). 13 | Example: 14 | 15 | ```bash 16 | docker-compose run --rm chirpstack-simulator make clean build 17 | ``` 18 | 19 | The binary will be located under `build/chirpstack-simulator`. 20 | 21 | ## Configuration 22 | 23 | For generating a configuration template, use the following command: 24 | 25 | ```bash 26 | ./build/chirpstack-simulator configfile > chirpstack-simulator.toml 27 | ``` 28 | 29 | ### Example 30 | 31 | ```toml 32 | [general] 33 | # Log level 34 | # 35 | # debug=5, info=4, warning=3, error=2, fatal=1, panic=0 36 | log_level=4 37 | 38 | 39 | # ChirpStack configuration. 40 | [chirpstack] 41 | 42 | # API configuration. 43 | # 44 | # This configuration is used to automatically create the: 45 | # * Device profile 46 | # * Gateways 47 | # * Application 48 | # * Devices 49 | [chirpstack.api] 50 | 51 | # JWT token. 52 | # 53 | # API key to connect to the ChirpStack API. This API key can created within 54 | # the ChirpStack web-interface. 55 | api_key="PUT_YOUR_API_KEY_HERE" 56 | 57 | # Server. 58 | # 59 | # This must point to the API interface of ChirpStack. 60 | # If the server is running on the same machine, keep this to the 61 | # default value. 62 | server="127.0.0.1:8080" 63 | 64 | # Insecure. 65 | # 66 | # Set this to true when the endpoint is not using TLS. 67 | insecure=true 68 | 69 | 70 | # MQTT integration configuration. 71 | # 72 | # This integration is used for counting the number of uplinks that are 73 | # published by the ChirpStack MQTT integration. 74 | [chirpstack.integration.mqtt] 75 | 76 | # MQTT server. 77 | server="tcp://127.0.0.1:1883" 78 | 79 | # Username. 80 | username="" 81 | 82 | # Password. 83 | password="" 84 | 85 | 86 | # MQTT gateway backend. 87 | [chirpstack.gateway.backend.mqtt] 88 | 89 | # MQTT server. 90 | server="tcp://127.0.0.1:1883" 91 | 92 | # Username. 93 | username="" 94 | 95 | # Password. 96 | password="" 97 | 98 | 99 | # Simulator configuration. 100 | [[simulator]] 101 | 102 | # Tenant ID. 103 | # 104 | # It is recommended to create a new tenant for simulations. 105 | tenant_id="PUT_YOUR_TENANT_ID_HERE" 106 | 107 | # Duration. 108 | # 109 | # This defines the duration of the simulation. If set to '0s', the simulation 110 | # will run until terminated. 111 | duration="5m" 112 | 113 | # Activation time. 114 | # 115 | # This is the time that the simulator takes to activate the devices. This 116 | # value must be less than the simulator duration. 117 | activation_time="1m" 118 | 119 | # Device configuration. 120 | [simulator.device] 121 | 122 | # Number of devices to simulate. 123 | count=1000 124 | 125 | # Uplink interval. 126 | uplink_interval="5m" 127 | 128 | # FPort. 129 | f_port=10 130 | 131 | # Payload (HEX encoded). 132 | payload="010203" 133 | 134 | # Frequency (Hz). 135 | frequency=868100000 136 | 137 | # Bandwidth (Hz). 138 | bandwidth=125000 139 | 140 | # Spreading-factor. 141 | spreading_factor=7 142 | 143 | # Gateway configuration. 144 | [simulator.gateway] 145 | 146 | # Min number of receiving gateways. 147 | min_count=3 148 | 149 | # Max number of receiving gateways. 150 | max_count=5 151 | 152 | # Event topic template. 153 | event_topic_template="eu868/gateway/{{ .GatewayID }}/event/{{ .Event }}" 154 | 155 | # Command topic template. 156 | command_topic_template="eu868/gateway/{{ .GatewayID }}/command/{{ .Command }}" 157 | 158 | 159 | # Prometheus metrics configuration. 160 | # 161 | # Using Prometheus (and Grafana), it is possible to visualize various 162 | # simulation metrics like: 163 | # * Join-Requests sent 164 | # * Join-Accepts received 165 | # * Uplinks sent (by the devices) 166 | # * Uplinks sent (by the gateways) 167 | # * Uplinks sent (by the ChirpStack MQTT integration) 168 | [prometheus] 169 | 170 | # IP:port to bind the Prometheus endpoint to. 171 | # 172 | # Metrics can be retrieved from /metrics. 173 | bind="0.0.0.0:9000" 174 | ``` 175 | 176 | ## Running the simulator 177 | 178 | To start the simulator, execute the following command: 179 | 180 | ```bash 181 | ./build/chirpstack-simulator -c chirpstack-simulator.toml 182 | ``` 183 | 184 | When a duration has been configured, then the simulation will stop after 185 | the given interval. Note that this does not terminate the process! This makes 186 | it possible to still read Prometheus metrics after the simulation has been 187 | completed. 188 | 189 | Regardless if a duration has been configured or not, the simulator can be 190 | terminated. When sending an interrupt signal once, the simulation will be 191 | terminated and the simulator will clean up the created gateways, devices, 192 | application and device-profile. When sending an interrupt for the second time, 193 | the simulator will be terminated immediately. 194 | 195 | ## Prometheus metrics 196 | 197 | The ChirpStack Simulator provides various metrics that can be collected using 198 | [Prometheus](https://prometheus.io/) and visualized using [Grafana](https://grafana.com/). 199 | 200 | * `device_uplink_count`: The number of uplinks sent by the devices 201 | * `device_join_request_count`: The number of join-requests sent by the devices 202 | * `device_join_accept_count`: The number of join-accepts received by the devices 203 | * `application_uplink_count`: The number of uplinks published by the application integration 204 | * `gateway_uplink_count`: The number of uplinks sent by the gateways 205 | * `gateway_downlink_count`: The number of downlinks received by the gateways 206 | 207 | ## License 208 | 209 | ChirpStack Simulator is distributed under the MIT license. See also 210 | [LICENSE](https://github.com/brocaar/chirpstack-simulator/blob/master/LICENSE). 211 | -------------------------------------------------------------------------------- /cmd/chirpstack-simulator/cmd/configfile.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/brocaar/chirpstack-simulator/internal/config" 11 | ) 12 | 13 | const configTemplate = `[general] 14 | # Log level 15 | # 16 | # debug=5, info=4, warning=3, error=2, fatal=1, panic=0 17 | log_level={{ .General.LogLevel }} 18 | 19 | 20 | # ChirpStack configuration. 21 | [chirpstack] 22 | 23 | # API configuration. 24 | # 25 | # This configuration is used to automatically create the: 26 | # * Device profile 27 | # * Gateways 28 | # * Application 29 | # * Devices 30 | [chirpstack.api] 31 | 32 | # API key. 33 | # 34 | # The API key can be obtained through the ChirpStack web-interface. 35 | api_key="{{ .ChirpStack.API.APIKey }}" 36 | 37 | # Server. 38 | # 39 | # This must point to the ChirpStack API interface. 40 | server="{{ .ChirpStack.API.Server }}" 41 | 42 | # Insecure. 43 | # 44 | # Set this to true when the endpoint is not using TLS. 45 | insecure={{ .ChirpStack.API.Insecure }} 46 | 47 | 48 | # MQTT integration configuration. 49 | # 50 | # This integration is used for counting the number of uplinks that are 51 | # published by the ChirpStack MQTT integration. 52 | [chirpstack.integration.mqtt] 53 | 54 | # MQTT server. 55 | server="{{ .ChirpStack.Integration.MQTT.Server }}" 56 | 57 | # Username. 58 | username="{{ .ChirpStack.Integration.MQTT.Username }}" 59 | 60 | # Password. 61 | password="{{ .ChirpStack.Integration.MQTT.Password }}" 62 | 63 | 64 | # MQTT gateway backend. 65 | [chirpstack.gateway.backend.mqtt] 66 | 67 | # MQTT server. 68 | server="{{ .ChirpStack.Gateway.Backend.MQTT.Server }}" 69 | 70 | # Username. 71 | username="{{ .ChirpStack.Gateway.Backend.MQTT.Username }}" 72 | 73 | # Password. 74 | password="{{ .ChirpStack.Gateway.Backend.MQTT.Password }}" 75 | 76 | 77 | # Simulator configuration. 78 | # 79 | # Example: 80 | # [[simulator]] 81 | # 82 | # # Tenant ID. 83 | # # 84 | # # It is recommended to create a new tenant in ChirpStack. 85 | # tenant_id="1f32476e-a112-4f00-bcc7-4aab4bfefa1d" 86 | # 87 | # # Duration. 88 | # # 89 | # # This defines the duration of the simulation. If set to '0s', the simulation 90 | # # will run until terminated. This includes the activation time. 91 | # duration="5m" 92 | # 93 | # # Activation time. 94 | # # 95 | # # This is the time that the simulator takes to activate the devices. This 96 | # # value must be less than the simulator duration. 97 | # activation_time="1m" 98 | # 99 | # # Device configuration. 100 | # [simulator.device] 101 | # 102 | # # Number of devices to simulate. 103 | # count=1000 104 | # 105 | # # Uplink interval. 106 | # uplink_interval="5m" 107 | # 108 | # # FPort. 109 | # f_port=10 110 | # 111 | # # Payload (HEX encoded). 112 | # payload="010203" 113 | # 114 | # # Frequency (Hz). 115 | # frequency=868100000 116 | # 117 | # # Bandwidth (Hz). 118 | # bandwidth=125000 119 | # 120 | # # Spreading-factor. 121 | # spreading_factor=7 122 | # 123 | # # Gateway configuration. 124 | # [simulator.gateway] 125 | # 126 | # # Event topic template. 127 | # event_topic_template="eu868/{{ "gateway/{{ .GatewayID }}/event/{{ .Event }}" }}" 128 | # 129 | # # Command topic template. 130 | # command_topic_template="eu868/{{ "gateway/{{ .GatewayID }}/command/{{ .Command }}" }}" 131 | # 132 | # # Min number of receiving gateways. 133 | # min_count=3 134 | # 135 | # # Max number of receiving gateways. 136 | # max_count=5 137 | {{ range $index, $element := .Simulator }} 138 | [[simulator]] 139 | service_profile_id="{{ $element.ServiceProfileID }}" 140 | duration="{{ $element.Duration }}" 141 | activation_time="{{ $element.ActivationTime }}" 142 | 143 | [simulator.device] 144 | count={{ $element.Device.Count }} 145 | uplink_interval="{{ $element.Device.UplinkInterval }}" 146 | f_port="{{ $element.Device.FPort }}" 147 | payload="{{ $element.Device.Payload }}" 148 | frequency={{ $element.Device.Frequency }} 149 | bandwidth={{ $element.Device.Bandwidth }} 150 | spreading_factor={{ $element.Device.SpreadingFactor }} 151 | 152 | [simulator.gateway] 153 | min_count={{ $element.Gateway.MinCount }} 154 | max_count={{ $element.Gateway.MaxCount }} 155 | event_topic_template="{{ $element.Gateway.EventTopicTemplate }}" 156 | command_topic_template="{{ $element.Gateway.CommandTopicTemplate }}" 157 | {{ end }} 158 | 159 | # Prometheus metrics configuration. 160 | # 161 | # Using Prometheus (and Grafana), it is possible to visualize various 162 | # simulation metrics like: 163 | # * Join-Requests sent 164 | # * Join-Accepts received 165 | # * Uplinks sent (by the devices) 166 | # * Uplinks sent (by the gateways) 167 | # * Uplinks sent (by the ChirpStack Application Server MQTT integration) 168 | [prometheus] 169 | 170 | # IP:port to bind the Prometheus endpoint to. 171 | # 172 | # Metrics can be retrieved from /metrics. 173 | bind="{{ .Prometheus.Bind }}" 174 | ` 175 | 176 | var configCmd = &cobra.Command{ 177 | Use: "configfile", 178 | Short: "Print the ChirpStack Network Server configuration file", 179 | RunE: func(cmd *cobra.Command, args []string) error { 180 | t := template.Must(template.New("config").Parse(configTemplate)) 181 | err := t.Execute(os.Stdout, &config.C) 182 | if err != nil { 183 | return errors.Wrap(err, "execute config template error") 184 | } 185 | return nil 186 | }, 187 | } 188 | -------------------------------------------------------------------------------- /cmd/chirpstack-simulator/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | 7 | "github.com/brocaar/chirpstack-simulator/internal/config" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var cfgFile string 14 | var version string 15 | 16 | // Execute executes the root command. 17 | func Execute(v string) { 18 | version = v 19 | if err := rootCmd.Execute(); err != nil { 20 | log.Fatal(err) 21 | } 22 | } 23 | 24 | var rootCmd = &cobra.Command{ 25 | Use: "chirpstack-simulator", 26 | Short: "ChirpStack Simulator", 27 | Long: `ChirpStack Simulator simulates device uplinks 28 | > documentation & support: https://www.chirpstack.io/ 29 | > source & copyright information: https://github.com/brocaar/chirpstack-simulator/`, 30 | RunE: run, 31 | } 32 | 33 | func init() { 34 | cobra.OnInitialize(initConfig) 35 | 36 | rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "path to configuration file (optional)") 37 | rootCmd.PersistentFlags().Int("log-level", 4, "debug=5, info=4, error=2, fatal=1, panic=0") 38 | 39 | viper.BindPFlag("general.log_level", rootCmd.PersistentFlags().Lookup("log-level")) 40 | 41 | viper.SetDefault("application_server.api.server", "127.0.0.1:8080") 42 | viper.SetDefault("application_server.integration.mqtt.server", "tcp://127.0.0.1:1883") 43 | viper.SetDefault("network_server.gateway.backend.mqtt.server", "tcp://127.0.0.1:1883") 44 | viper.SetDefault("prometheus.bind", "0.0.0.0:9000") 45 | 46 | rootCmd.AddCommand(versionCmd) 47 | rootCmd.AddCommand(configCmd) 48 | } 49 | 50 | func initConfig() { 51 | config.Version = version 52 | 53 | if cfgFile != "" { 54 | b, err := ioutil.ReadFile(cfgFile) 55 | if err != nil { 56 | log.WithError(err).WithField("config", cfgFile).Fatal("error loading config file") 57 | } 58 | viper.SetConfigType("toml") 59 | if err := viper.ReadConfig(bytes.NewBuffer(b)); err != nil { 60 | log.WithError(err).WithField("config", cfgFile).Fatal("error loading config file") 61 | } 62 | } else { 63 | viper.SetConfigName("chirpstack-simulator") 64 | viper.AddConfigPath(".") 65 | viper.AddConfigPath("$HOME/.config/chirpstack-simulator") 66 | viper.AddConfigPath("/etc/chirpstack-simulator") 67 | if err := viper.ReadInConfig(); err != nil { 68 | switch err.(type) { 69 | case viper.ConfigFileNotFoundError: 70 | default: 71 | log.WithError(err).Fatal("read configuration file error") 72 | } 73 | } 74 | } 75 | 76 | if err := viper.Unmarshal(&config.C); err != nil { 77 | log.WithError(err).Fatal("unmarshal config error") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /cmd/chirpstack-simulator/cmd/root_run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "syscall" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | 16 | "github.com/brocaar/chirpstack-simulator/internal/as" 17 | "github.com/brocaar/chirpstack-simulator/internal/config" 18 | "github.com/brocaar/chirpstack-simulator/internal/ns" 19 | "github.com/brocaar/chirpstack-simulator/internal/simulator" 20 | ) 21 | 22 | func run(cnd *cobra.Command, args []string) error { 23 | tasks := []func(context.Context, *sync.WaitGroup) error{ 24 | setLogLevel, 25 | printStartMessage, 26 | setupASAPIClient, 27 | setupASIntegration, 28 | setupNSIntegration, 29 | setupPrometheus, 30 | startSimulator, 31 | } 32 | 33 | var wg sync.WaitGroup 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | 36 | for _, t := range tasks { 37 | if err := t(ctx, &wg); err != nil { 38 | log.Fatal(err) 39 | } 40 | } 41 | 42 | exitChan := make(chan struct{}) 43 | sigChan := make(chan os.Signal) 44 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 45 | <-sigChan 46 | go func() { 47 | cancel() 48 | wg.Wait() 49 | exitChan <- struct{}{} 50 | }() 51 | cancel() 52 | select { 53 | case <-exitChan: 54 | case s := <-sigChan: 55 | log.WithField("signal", s).Info("signal received, terminating") 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func setLogLevel(ctx context.Context, wg *sync.WaitGroup) error { 62 | log.SetLevel(log.Level(uint8(config.C.General.LogLevel))) 63 | return nil 64 | } 65 | 66 | func printStartMessage(ctx context.Context, wg *sync.WaitGroup) error { 67 | log.WithFields(log.Fields{ 68 | "version": version, 69 | "docs": "https://www.chirpstack.io/", 70 | }).Info("starting ChirpStack Simulator") 71 | return nil 72 | } 73 | 74 | func setupASAPIClient(ctx context.Context, wg *sync.WaitGroup) error { 75 | return as.Setup(config.C) 76 | } 77 | 78 | func setupASIntegration(ctx context.Context, wg *sync.WaitGroup) error { 79 | return nil 80 | } 81 | 82 | func setupNSIntegration(ctx context.Context, wg *sync.WaitGroup) error { 83 | return ns.Setup(config.C) 84 | } 85 | 86 | func setupPrometheus(ctx context.Context, wg *sync.WaitGroup) error { 87 | log.WithFields(log.Fields{ 88 | "bind": config.C.Prometheus.Bind, 89 | }).Info("starting Prometheus endpoint server") 90 | 91 | mux := http.NewServeMux() 92 | mux.Handle("/metrics", promhttp.Handler()) 93 | 94 | server := http.Server{ 95 | Handler: mux, 96 | Addr: config.C.Prometheus.Bind, 97 | } 98 | 99 | go func() { 100 | err := server.ListenAndServe() 101 | log.WithError(err).Error("prometheus endpoint server error") 102 | }() 103 | 104 | return nil 105 | } 106 | 107 | func startSimulator(ctx context.Context, wg *sync.WaitGroup) error { 108 | if err := simulator.Start(ctx, wg, config.C); err != nil { 109 | return errors.Wrap(err, "start simulator error") 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /cmd/chirpstack-simulator/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 Network Server version", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | fmt.Println(version) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cmd/chirpstack-simulator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/brocaar/chirpstack-simulator/cmd/chirpstack-simulator/cmd" 4 | 5 | var version string // set by the compiler 6 | 7 | func main() { 8 | cmd.Execute(version) 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | chirpstack-simulator: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-devel 7 | command: make 8 | volumes: 9 | - ./:/chirpstack-simulator 10 | -------------------------------------------------------------------------------- /examples/single_uplink/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "sync" 7 | "time" 8 | 9 | "github.com/brocaar/chirpstack-simulator/simulator" 10 | "github.com/brocaar/lorawan" 11 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // This example simulates an OTAA activation and the sending of a single uplink 16 | // frame, after which the simulation terminates. 17 | func main() { 18 | gatewayID := lorawan.EUI64{1, 1, 1, 1, 1, 1, 1, 1} 19 | devEUI := lorawan.EUI64{2, 1, 1, 1, 1, 1, 1, 1} 20 | appKey := lorawan.AES128Key{3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} 21 | 22 | var wg sync.WaitGroup 23 | ctx := context.Background() 24 | 25 | sgw, err := simulator.NewGateway( 26 | simulator.WithMQTTCredentials("localhost:1883", "", ""), 27 | simulator.WithGatewayID(gatewayID), 28 | simulator.WithEventTopicTemplate("eu868/gateway/{{ .GatewayID }}/event/{{ .Event }}"), 29 | simulator.WithCommandTopicTemplate("eu868/gateway/{{ .GatewayID }}/command/{{ .Command }}"), 30 | ) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | _, err = simulator.NewDevice(ctx, &wg, 36 | simulator.WithDevEUI(devEUI), 37 | simulator.WithAppKey(appKey), 38 | simulator.WithRandomDevNonce(), 39 | simulator.WithUplinkInterval(time.Second), 40 | simulator.WithUplinkCount(1), 41 | simulator.WithUplinkPayload(true, 10, []byte{1, 2, 3}), 42 | simulator.WithUplinkTXInfo(gw.UplinkTxInfo{ 43 | Frequency: 868100000, 44 | Modulation: &gw.Modulation{ 45 | Parameters: &gw.Modulation_Lora{ 46 | Lora: &gw.LoraModulationInfo{ 47 | Bandwidth: 125000, 48 | SpreadingFactor: 7, 49 | CodeRate: gw.CodeRate_CR_4_6, 50 | }, 51 | }, 52 | }, 53 | }), 54 | simulator.WithGateways([]*simulator.Gateway{sgw}), 55 | simulator.WithDownlinkHandlerFunc(func(conf, ack bool, fCntDown uint32, fPort uint8, data []byte) error { 56 | log.WithFields(log.Fields{ 57 | "ack": ack, 58 | "fcnt_down": fCntDown, 59 | "f_port": fPort, 60 | "data": hex.EncodeToString(data), 61 | }).Info("WithDownlinkHandlerFunc triggered") 62 | 63 | return nil 64 | }), 65 | ) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | wg.Wait() 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brocaar/chirpstack-simulator 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/brocaar/lorawan v0.0.0-20191105091820-9ed596703a6c 7 | github.com/chirpstack/chirpstack/api/go/v4 v4.0.0-rc.2 8 | github.com/eclipse/paho.mqtt.golang v1.2.0 9 | github.com/gofrs/uuid v3.2.0+incompatible 10 | github.com/golang/protobuf v1.5.2 11 | github.com/pkg/errors v0.8.1 12 | github.com/prometheus/client_golang v0.9.3 13 | github.com/sirupsen/logrus v1.4.2 14 | github.com/smartystreets/assertions v1.0.0 // indirect 15 | github.com/spf13/cobra v0.0.5 16 | github.com/spf13/viper v1.5.0 17 | golang.org/x/text v0.3.2 // indirect 18 | google.golang.org/grpc v1.45.0 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5/go.mod h1:w5D10RxC0NmPYxmQ438CC1S07zaC1zpvuNW7s5sUk2Q= 6 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 10 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 13 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 14 | github.com/brocaar/lorawan v0.0.0-20191105091820-9ed596703a6c h1:yRK+Z564Rqb+eu3Kda+0GefV6KkkcF8nAqoKqNLQp+w= 15 | github.com/brocaar/lorawan v0.0.0-20191105091820-9ed596703a6c/go.mod h1:VgyRGAJ/wl1JfqZZmNlCTM8fyaIKF11YvYAGIVtedL8= 16 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 17 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 18 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/chirpstack/chirpstack/api/go/v4 v4.0.0-rc.2 h1:QofCUXjgY3EUxKDyTIjPHHW5/+4/c8P6o3valxuD2/M= 20 | github.com/chirpstack/chirpstack/api/go/v4 v4.0.0-rc.2/go.mod h1:KBW7imf70O9ifrMmoFH8+dn0+MUFS1PdC5shXH7W3dI= 21 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 22 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 23 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 24 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 25 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 26 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 27 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 28 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 29 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 30 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 31 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 32 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 33 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 34 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 39 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 40 | github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= 41 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 42 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 43 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 44 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 45 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 46 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 47 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 48 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 49 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 50 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 51 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 52 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 53 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 54 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 55 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 56 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 57 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 58 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 59 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 60 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 61 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 62 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 66 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 67 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 68 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 69 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 70 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 71 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 72 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 73 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 74 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 75 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 76 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 77 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 78 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 79 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 80 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 81 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 82 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 84 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 85 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 86 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 87 | github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f h1:4Gslotqbs16iAg+1KR/XdabIfq8TlAWHdwS5QJFksLc= 88 | github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 89 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 90 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 91 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 92 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 93 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 94 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 95 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 96 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 97 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 98 | github.com/jacobsa/crypto v0.0.0-20180924003735-d95898ceee07 h1:/PaS1RNKtbBEndIvzCqIgYh6GAH9ZFc8Mj4tVRVyfOA= 99 | github.com/jacobsa/crypto v0.0.0-20180924003735-d95898ceee07/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU= 100 | github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= 101 | github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd/go.mod h1:TlmyIZDpGmwRoTWiakdr+HA1Tukze6C6XbRVidYq02M= 102 | github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff h1:2xRHTvkpJ5zJmglXLRqHiZQNjUoOkhUyhTAhEQvPAWw= 103 | github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff/go.mod h1:gJWba/XXGl0UoOmBQKRWCJdHrr3nE0T65t6ioaj3mLI= 104 | github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ulwVIHVDWFHp8eIXmbfSExkvdn9qMXI= 105 | github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11/go.mod h1:+DBdDyfoO2McrOyDemRBq0q9CMEByef7sYl7JH5Q3BI= 106 | github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb h1:uSWBjJdMf47kQlXMwWEfmc864bA1wAC+Kl3ApryuG9Y= 107 | github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb/go.mod h1:ivcmUvxXWjb27NsPEaiYK7AidlZXS7oQ5PowUS9z3I4= 108 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 109 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 110 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 111 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 112 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 113 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 114 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 115 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 116 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 117 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 118 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 119 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 120 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 121 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 122 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 123 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 124 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 125 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 126 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 127 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 128 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 129 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 130 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 131 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 132 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 133 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 134 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 135 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 136 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 137 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 138 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 139 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 140 | github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= 141 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 142 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 143 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 144 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= 145 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 146 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 147 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= 148 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 149 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 150 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= 151 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 152 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 153 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 154 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 155 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 156 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 157 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 158 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 159 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 160 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 161 | github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 162 | github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= 163 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 164 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= 165 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 166 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 167 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 168 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 169 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 170 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 171 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 172 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= 173 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 174 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 175 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 176 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 177 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 178 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 179 | github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4= 180 | github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4= 181 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 182 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 183 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 184 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 185 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 186 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 187 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 188 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 189 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 190 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 191 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 192 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 193 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 194 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 195 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 196 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 197 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 198 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 199 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 200 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 201 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 202 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 203 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 204 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 205 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 206 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 207 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 208 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 209 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 212 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 213 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 214 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 215 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 216 | golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 217 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 218 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 219 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= 220 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 221 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 222 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 223 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 225 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 226 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 227 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 229 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 230 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 231 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 232 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 233 | golang.org/x/sys v0.0.0-20190402054613-e4093980e83e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 237 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 238 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 239 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 240 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 241 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 242 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 243 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 244 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 245 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 246 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 247 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 248 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 249 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 250 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 251 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 252 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 253 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 254 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 255 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 256 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 257 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 258 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 259 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 260 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 261 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 262 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 263 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 264 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 265 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 266 | google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= 267 | google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= 268 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= 269 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 270 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 271 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 272 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 273 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 274 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 275 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 276 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 277 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 278 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 279 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 280 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 281 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 282 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 283 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 284 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 285 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 286 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 287 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 288 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 289 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 290 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 291 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 292 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 293 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 294 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 295 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 296 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 297 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 298 | -------------------------------------------------------------------------------- /internal/as/api.go: -------------------------------------------------------------------------------- 1 | package as 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "time" 7 | 8 | mqtt "github.com/eclipse/paho.mqtt.golang" 9 | "github.com/pkg/errors" 10 | log "github.com/sirupsen/logrus" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials" 13 | 14 | "github.com/brocaar/chirpstack-simulator/internal/config" 15 | "github.com/chirpstack/chirpstack/api/go/v4/api" 16 | ) 17 | 18 | var clientConn *grpc.ClientConn 19 | var mqttClient mqtt.Client 20 | 21 | type jwtCredentials struct { 22 | token string 23 | } 24 | 25 | func (j *jwtCredentials) GetRequestMetadata(ctx context.Context, url ...string) (map[string]string, error) { 26 | return map[string]string{ 27 | "authorization": "Bearer " + j.token, 28 | }, nil 29 | } 30 | 31 | func (j *jwtCredentials) RequireTransportSecurity() bool { 32 | return false 33 | } 34 | 35 | // Setup configures the AS API client. 36 | func Setup(c config.Config) error { 37 | conf := c.ChirpStack 38 | 39 | // connect gRPC 40 | log.WithFields(log.Fields{ 41 | "server": conf.API.Server, 42 | "insecure": conf.API.Insecure, 43 | }).Info("as: connecting api client") 44 | 45 | dialOpts := []grpc.DialOption{ 46 | grpc.WithBlock(), 47 | grpc.WithPerRPCCredentials(&jwtCredentials{token: conf.API.APIKey}), 48 | } 49 | 50 | if conf.API.Insecure { 51 | dialOpts = append(dialOpts, grpc.WithInsecure()) 52 | } else { 53 | dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) 54 | } 55 | 56 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 57 | defer cancel() 58 | 59 | conn, err := grpc.DialContext(ctx, conf.API.Server, dialOpts...) 60 | if err != nil { 61 | return errors.Wrap(err, "grpc dial error") 62 | } 63 | 64 | clientConn = conn 65 | 66 | // connect MQTT 67 | opts := mqtt.NewClientOptions() 68 | opts.AddBroker(conf.Integration.MQTT.Server) 69 | opts.SetUsername(conf.Integration.MQTT.Username) 70 | opts.SetPassword(conf.Integration.MQTT.Password) 71 | opts.SetCleanSession(true) 72 | opts.SetAutoReconnect(true) 73 | 74 | log.WithFields(log.Fields{ 75 | "server": conf.Integration.MQTT.Server, 76 | }).Info("as: connecting to mqtt broker") 77 | 78 | mqttClient = mqtt.NewClient(opts) 79 | if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { 80 | return errors.Wrap(token.Error(), "mqtt client connect error") 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func Tenant() api.TenantServiceClient { 87 | return api.NewTenantServiceClient(clientConn) 88 | } 89 | 90 | func Gateway() api.GatewayServiceClient { 91 | return api.NewGatewayServiceClient(clientConn) 92 | } 93 | 94 | func DeviceProfile() api.DeviceProfileServiceClient { 95 | return api.NewDeviceProfileServiceClient(clientConn) 96 | } 97 | 98 | func Application() api.ApplicationServiceClient { 99 | return api.NewApplicationServiceClient(clientConn) 100 | } 101 | 102 | func Device() api.DeviceServiceClient { 103 | return api.NewDeviceServiceClient(clientConn) 104 | } 105 | 106 | // MQTTClient returns the MQTT client for the Application Server MQTT integration. 107 | func MQTTClient() mqtt.Client { 108 | return mqttClient 109 | } 110 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Version defines the version. 8 | var Version string 9 | 10 | // Config defines the configuration. 11 | type Config struct { 12 | General struct { 13 | LogLevel int `mapstructure:"log_level"` 14 | } 15 | 16 | ChirpStack struct { 17 | API struct { 18 | APIKey string `mapstructure:"api_key"` 19 | Server string `mapstructure:"server"` 20 | Insecure bool `mapstructure:"insecure"` 21 | } `mapstructure:"api"` 22 | 23 | Integration struct { 24 | MQTT struct { 25 | Server string `mapstructure:"server"` 26 | Username string `mapstructure:"username"` 27 | Password string `mapstructure:"password"` 28 | } `mapstructure:"mqtt"` 29 | } `mapstructure:"integration"` 30 | 31 | Gateway struct { 32 | Backend struct { 33 | MQTT struct { 34 | Server string `mapstructure:"server"` 35 | Username string `mapstructure:"username"` 36 | Password string `mapstructure:"password"` 37 | } `mapstructure:"mqtt"` 38 | } `mapstructure:"backend"` 39 | } `mapstructure:"gateway"` 40 | } `mapstructure:"chirpstack"` 41 | 42 | Simulator []struct { 43 | TenantID string `mapstructure:"tenant_id"` 44 | Duration time.Duration `mapstructure:"duration"` 45 | ActivationTime time.Duration `mapstructure:"activation_time"` 46 | 47 | Device struct { 48 | Count int `mapstructure:"count"` 49 | UplinkInterval time.Duration `mapstructure:"uplink_interval"` 50 | FPort uint8 `mapstructure:"f_port"` 51 | Payload string `mapstructure:"payload"` 52 | Frequency int `mapstructure:"frequency"` 53 | Bandwidth int `mapstructure:"bandwidth"` 54 | SpreadingFactor int `mapstructure:"spreading_factor"` 55 | } `mapstructure:"device"` 56 | 57 | Gateway struct { 58 | MinCount int `mapstructure:"min_count"` 59 | MaxCount int `mapstructure:"max_count"` 60 | EventTopicTemplate string `mapstructure:"event_topic_template"` 61 | CommandTopicTemplate string `mapstructure:"command_topic_template"` 62 | } `mapstructure:"gateway"` 63 | } `mapstructure:"simulator"` 64 | 65 | Prometheus struct { 66 | Bind string `mapstructure:"bind"` 67 | } `mapstructure:"prometheus"` 68 | } 69 | 70 | type DeviceConfig struct { 71 | DevEUI string `mapstructure:"dev_eui"` 72 | AppKey string `mapstructure:"app_key"` 73 | UplinkInterval time.Duration `mapstructure:"uplink_interval"` 74 | FPort uint8 `mapstructure:"f_port"` 75 | Payload string `mapstructure:"payload"` 76 | } 77 | 78 | // C holds the global configuration. 79 | var C Config 80 | -------------------------------------------------------------------------------- /internal/ns/mqtt.go: -------------------------------------------------------------------------------- 1 | package ns 2 | 3 | import ( 4 | mqtt "github.com/eclipse/paho.mqtt.golang" 5 | "github.com/pkg/errors" 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/brocaar/chirpstack-simulator/internal/config" 9 | ) 10 | 11 | var mqttClient mqtt.Client 12 | 13 | // Setup configures the NS MQTT gateway backend. 14 | func Setup(c config.Config) error { 15 | conf := c.ChirpStack.Gateway.Backend.MQTT 16 | 17 | opts := mqtt.NewClientOptions() 18 | opts.AddBroker(conf.Server) 19 | opts.SetUsername(conf.Username) 20 | opts.SetPassword(conf.Password) 21 | opts.SetCleanSession(true) 22 | opts.SetAutoReconnect(true) 23 | 24 | log.WithFields(log.Fields{ 25 | "server": conf.Server, 26 | }).Info("ns: connecting to mqtt broker") 27 | 28 | mqttClient = mqtt.NewClient(opts) 29 | if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { 30 | return errors.Wrap(token.Error(), "mqtt client connect error") 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func Client() mqtt.Client { 37 | return mqttClient 38 | } 39 | -------------------------------------------------------------------------------- /internal/simulator/metrics.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | auc = promauto.NewCounter(prometheus.CounterOpts{ 10 | Name: "application_uplink_count", 11 | Help: "The number of uplinks published by the application integration.", 12 | }) 13 | ) 14 | 15 | func applicationUplinkCounter() prometheus.Counter { 16 | return auc 17 | } 18 | -------------------------------------------------------------------------------- /internal/simulator/session_keys.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "crypto/aes" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/brocaar/lorawan" 10 | ) 11 | 12 | // getAppSKey returns appSKey. 13 | func getAppSKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 14 | return getSKey(optNeg, 0x02, nwkKey, netID, joinEUI, joinNonce, devNonce) 15 | } 16 | 17 | // getFNwkSIntKey returns the FNwkSIntKey. 18 | // For LoRaWAN 1.0: SNwkSIntKey = NwkSEncKey = FNwkSIntKey = NwkSKey 19 | func getFNwkSIntKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 20 | return getSKey(optNeg, 0x01, nwkKey, netID, joinEUI, joinNonce, devNonce) 21 | } 22 | 23 | func getSKey(optNeg bool, typ byte, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 24 | var key lorawan.AES128Key 25 | b := make([]byte, 16) 26 | b[0] = typ 27 | 28 | netIDB, err := netID.MarshalBinary() 29 | if err != nil { 30 | return key, errors.Wrap(err, "marshal binary error") 31 | } 32 | 33 | joinEUIB, err := joinEUI.MarshalBinary() 34 | if err != nil { 35 | return key, errors.Wrap(err, "marshal binary error") 36 | } 37 | 38 | joinNonceB, err := joinNonce.MarshalBinary() 39 | if err != nil { 40 | return key, errors.Wrap(err, "marshal binary error") 41 | } 42 | 43 | devNonceB, err := devNonce.MarshalBinary() 44 | if err != nil { 45 | return key, errors.Wrap(err, "marshal binary error") 46 | } 47 | 48 | if optNeg { 49 | copy(b[1:4], joinNonceB) 50 | copy(b[4:12], joinEUIB) 51 | copy(b[12:14], devNonceB) 52 | } else { 53 | copy(b[1:4], joinNonceB) 54 | copy(b[4:7], netIDB) 55 | copy(b[7:9], devNonceB) 56 | } 57 | 58 | block, err := aes.NewCipher(nwkKey[:]) 59 | if err != nil { 60 | return key, err 61 | } 62 | if block.BlockSize() != len(b) { 63 | return key, fmt.Errorf("block-size of %d bytes is expected", len(b)) 64 | } 65 | block.Encrypt(key[:], b) 66 | 67 | return key, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/simulator/simulator.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "fmt" 8 | mrand "math/rand" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | "syscall" 13 | "time" 14 | 15 | mqtt "github.com/eclipse/paho.mqtt.golang" 16 | "github.com/gofrs/uuid" 17 | "github.com/pkg/errors" 18 | log "github.com/sirupsen/logrus" 19 | 20 | "github.com/brocaar/chirpstack-simulator/internal/as" 21 | "github.com/brocaar/chirpstack-simulator/internal/config" 22 | "github.com/brocaar/chirpstack-simulator/internal/ns" 23 | "github.com/brocaar/chirpstack-simulator/simulator" 24 | "github.com/brocaar/lorawan" 25 | "github.com/chirpstack/chirpstack/api/go/v4/api" 26 | "github.com/chirpstack/chirpstack/api/go/v4/common" 27 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 28 | ) 29 | 30 | // Start starts the simulator. 31 | func Start(ctx context.Context, wg *sync.WaitGroup, c config.Config) error { 32 | for i, c := range c.Simulator { 33 | log.WithFields(log.Fields{ 34 | "i": i, 35 | }).Info("simulator: starting simulation") 36 | 37 | wg.Add(1) 38 | 39 | pl, err := hex.DecodeString(c.Device.Payload) 40 | if err != nil { 41 | return errors.Wrap(err, "decode payload error") 42 | } 43 | 44 | sim := simulation{ 45 | ctx: ctx, 46 | wg: wg, 47 | tenantID: c.TenantID, 48 | deviceCount: c.Device.Count, 49 | activationTime: c.ActivationTime, 50 | uplinkInterval: c.Device.UplinkInterval, 51 | fPort: c.Device.FPort, 52 | payload: pl, 53 | frequency: c.Device.Frequency, 54 | bandwidth: c.Device.Bandwidth, 55 | spreadingFactor: c.Device.SpreadingFactor, 56 | duration: c.Duration, 57 | gatewayMinCount: c.Gateway.MinCount, 58 | gatewayMaxCount: c.Gateway.MaxCount, 59 | deviceAppKeys: make(map[lorawan.EUI64]lorawan.AES128Key), 60 | eventTopicTemplate: c.Gateway.EventTopicTemplate, 61 | commandTopicTemplate: c.Gateway.CommandTopicTemplate, 62 | } 63 | 64 | go sim.start() 65 | } 66 | 67 | return nil 68 | } 69 | 70 | type simulation struct { 71 | ctx context.Context 72 | wg *sync.WaitGroup 73 | tenantID string 74 | deviceCount int 75 | gatewayMinCount int 76 | gatewayMaxCount int 77 | duration time.Duration 78 | 79 | fPort uint8 80 | payload []byte 81 | activationTime time.Duration 82 | uplinkInterval time.Duration 83 | frequency int 84 | bandwidth int 85 | spreadingFactor int 86 | 87 | tenant *api.Tenant 88 | deviceProfileID uuid.UUID 89 | applicationID string 90 | gatewayIDs []lorawan.EUI64 91 | deviceAppKeysMutex sync.Mutex 92 | deviceAppKeys map[lorawan.EUI64]lorawan.AES128Key 93 | eventTopicTemplate string 94 | commandTopicTemplate string 95 | } 96 | 97 | func (s *simulation) start() { 98 | if err := s.init(); err != nil { 99 | log.WithError(err).Error("simulator: init simulation error") 100 | } 101 | 102 | if err := s.runSimulation(); err != nil { 103 | log.WithError(err).Error("simulator: simulation error") 104 | } 105 | 106 | log.Info("simulator: simulation completed") 107 | 108 | if err := s.tearDown(); err != nil { 109 | log.WithError(err).Error("simulator: tear-down simulation error") 110 | } 111 | 112 | s.wg.Done() 113 | 114 | log.Info("simulation: tear-down completed") 115 | } 116 | 117 | func (s *simulation) init() error { 118 | log.Info("simulation: setting up") 119 | 120 | if err := s.setupTenant(); err != nil { 121 | return err 122 | } 123 | 124 | if err := s.setupGateways(); err != nil { 125 | return err 126 | } 127 | 128 | if err := s.setupDeviceProfile(); err != nil { 129 | return err 130 | } 131 | 132 | if err := s.setupApplication(); err != nil { 133 | return err 134 | } 135 | 136 | if err := s.setupDevices(); err != nil { 137 | return err 138 | } 139 | 140 | if err := s.setupApplicationIntegration(); err != nil { 141 | return err 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (s *simulation) tearDown() error { 148 | log.Info("simulation: cleaning up") 149 | 150 | if err := s.tearDownApplicationIntegration(); err != nil { 151 | return err 152 | } 153 | 154 | if err := s.tearDownDevices(); err != nil { 155 | return err 156 | } 157 | 158 | if err := s.tearDownApplication(); err != nil { 159 | return err 160 | } 161 | 162 | if err := s.tearDownDeviceProfile(); err != nil { 163 | return err 164 | } 165 | 166 | if err := s.tearDownGateways(); err != nil { 167 | return err 168 | } 169 | 170 | return nil 171 | } 172 | 173 | func (s *simulation) runSimulation() error { 174 | var gateways []*simulator.Gateway 175 | var devices []*simulator.Device 176 | 177 | for _, gatewayID := range s.gatewayIDs { 178 | gw, err := simulator.NewGateway( 179 | simulator.WithGatewayID(gatewayID), 180 | simulator.WithMQTTClient(ns.Client()), 181 | simulator.WithEventTopicTemplate(s.eventTopicTemplate), 182 | simulator.WithCommandTopicTemplate(s.commandTopicTemplate), 183 | ) 184 | if err != nil { 185 | return errors.Wrap(err, "new gateway error") 186 | } 187 | gateways = append(gateways, gw) 188 | } 189 | 190 | var wg sync.WaitGroup 191 | ctx, cancel := context.WithCancel(s.ctx) 192 | if s.duration != 0 { 193 | ctx, cancel = context.WithTimeout(ctx, s.duration) 194 | } 195 | defer cancel() 196 | 197 | for devEUI, appKey := range s.deviceAppKeys { 198 | devGateways := make(map[int]*simulator.Gateway) 199 | devNumGateways := s.gatewayMinCount + mrand.Intn(s.gatewayMaxCount-s.gatewayMinCount+1) 200 | 201 | for len(devGateways) < devNumGateways { 202 | // pick random gateway index 203 | n := mrand.Intn(len(gateways)) 204 | devGateways[n] = gateways[n] 205 | } 206 | 207 | var gws []*simulator.Gateway 208 | for k := range devGateways { 209 | gws = append(gws, devGateways[k]) 210 | } 211 | 212 | d, err := simulator.NewDevice(ctx, &wg, 213 | simulator.WithDevEUI(devEUI), 214 | simulator.WithAppKey(appKey), 215 | simulator.WithUplinkInterval(s.uplinkInterval), 216 | simulator.WithOTAADelay(time.Duration(mrand.Int63n(int64(s.activationTime)))), 217 | simulator.WithUplinkPayload(false, s.fPort, s.payload), 218 | simulator.WithGateways(gws), 219 | simulator.WithUplinkTXInfo(gw.UplinkTxInfo{ 220 | Frequency: uint32(s.frequency), 221 | Modulation: &gw.Modulation{ 222 | Parameters: &gw.Modulation_Lora{ 223 | Lora: &gw.LoraModulationInfo{ 224 | Bandwidth: uint32(s.bandwidth), 225 | SpreadingFactor: uint32(s.spreadingFactor), 226 | CodeRate: gw.CodeRate_CR_4_5, 227 | }, 228 | }, 229 | }, 230 | }), 231 | ) 232 | if err != nil { 233 | return errors.Wrap(err, "new device error") 234 | } 235 | 236 | devices = append(devices, d) 237 | } 238 | 239 | go func() { 240 | sigChan := make(chan os.Signal) 241 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 242 | 243 | select { 244 | case sig := <-sigChan: 245 | log.WithField("signal", sig).Info("signal received, stopping simulators") 246 | cancel() 247 | case <-ctx.Done(): 248 | } 249 | }() 250 | 251 | wg.Wait() 252 | 253 | return nil 254 | } 255 | 256 | func (s *simulation) setupTenant() error { 257 | log.WithFields(log.Fields{ 258 | "tenant_id": s.tenantID, 259 | }).Info("simulator: retrieving tenant") 260 | t, err := as.Tenant().Get(context.Background(), &api.GetTenantRequest{ 261 | Id: s.tenantID, 262 | }) 263 | if err != nil { 264 | return errors.Wrap(err, "get tenant error") 265 | } 266 | s.tenant = t.GetTenant() 267 | 268 | return nil 269 | } 270 | 271 | func (s *simulation) setupGateways() error { 272 | log.Info("simulator: creating gateways") 273 | 274 | for i := 0; i < s.gatewayMaxCount; i++ { 275 | var gatewayID lorawan.EUI64 276 | if _, err := rand.Read(gatewayID[:]); err != nil { 277 | return errors.Wrap(err, "read random bytes error") 278 | } 279 | 280 | _, err := as.Gateway().Create(context.Background(), &api.CreateGatewayRequest{ 281 | Gateway: &api.Gateway{ 282 | GatewayId: gatewayID.String(), 283 | Name: gatewayID.String(), 284 | Description: gatewayID.String(), 285 | TenantId: s.tenant.GetId(), 286 | Location: &common.Location{}, 287 | }, 288 | }) 289 | if err != nil { 290 | return errors.Wrap(err, "create gateway error") 291 | } 292 | 293 | s.gatewayIDs = append(s.gatewayIDs, gatewayID) 294 | } 295 | 296 | return nil 297 | } 298 | 299 | func (s *simulation) tearDownGateways() error { 300 | log.Info("simulator: tear-down gateways") 301 | 302 | for _, gatewayID := range s.gatewayIDs { 303 | _, err := as.Gateway().Delete(context.Background(), &api.DeleteGatewayRequest{ 304 | GatewayId: gatewayID.String(), 305 | }) 306 | if err != nil { 307 | return errors.Wrap(err, "delete gateway error") 308 | } 309 | } 310 | 311 | return nil 312 | } 313 | 314 | func (s *simulation) setupDeviceProfile() error { 315 | log.Info("simulator: creating device-profile") 316 | 317 | dpName, _ := uuid.NewV4() 318 | 319 | resp, err := as.DeviceProfile().Create(context.Background(), &api.CreateDeviceProfileRequest{ 320 | DeviceProfile: &api.DeviceProfile{ 321 | Name: dpName.String(), 322 | TenantId: s.tenant.GetId(), 323 | MacVersion: common.MacVersion_LORAWAN_1_0_3, 324 | RegParamsRevision: common.RegParamsRevision_B, 325 | SupportsOtaa: true, 326 | Region: common.Region_EU868, 327 | AdrAlgorithmId: "default", 328 | }, 329 | }) 330 | if err != nil { 331 | return errors.Wrap(err, "create device-profile error") 332 | } 333 | 334 | dpID, err := uuid.FromString(resp.Id) 335 | if err != nil { 336 | return err 337 | } 338 | s.deviceProfileID = dpID 339 | 340 | return nil 341 | } 342 | 343 | func (s *simulation) tearDownDeviceProfile() error { 344 | log.Info("simulator: tear-down device-profile") 345 | 346 | _, err := as.DeviceProfile().Delete(context.Background(), &api.DeleteDeviceProfileRequest{ 347 | Id: s.deviceProfileID.String(), 348 | }) 349 | if err != nil { 350 | return errors.Wrap(err, "delete device-profile error") 351 | } 352 | 353 | return nil 354 | } 355 | 356 | func (s *simulation) setupApplication() error { 357 | log.Info("simulator: init application") 358 | 359 | appName, err := uuid.NewV4() 360 | if err != nil { 361 | return err 362 | } 363 | 364 | createAppResp, err := as.Application().Create(context.Background(), &api.CreateApplicationRequest{ 365 | Application: &api.Application{ 366 | Name: appName.String(), 367 | Description: appName.String(), 368 | TenantId: s.tenant.GetId(), 369 | }, 370 | }) 371 | if err != nil { 372 | return errors.Wrap(err, "create applicaiton error") 373 | } 374 | 375 | s.applicationID = createAppResp.Id 376 | return nil 377 | } 378 | 379 | func (s *simulation) tearDownApplication() error { 380 | log.Info("simulator: tear-down application") 381 | 382 | _, err := as.Application().Delete(context.Background(), &api.DeleteApplicationRequest{ 383 | Id: s.applicationID, 384 | }) 385 | if err != nil { 386 | return errors.Wrap(err, "delete application error") 387 | } 388 | return nil 389 | } 390 | 391 | func (s *simulation) setupDevices() error { 392 | log.Info("simulator: init devices") 393 | 394 | var wg sync.WaitGroup 395 | 396 | for i := 0; i < s.deviceCount; i++ { 397 | wg.Add(1) 398 | 399 | go func() { 400 | var devEUI lorawan.EUI64 401 | var appKey lorawan.AES128Key 402 | 403 | if _, err := rand.Read(devEUI[:]); err != nil { 404 | log.Fatal(err) 405 | } 406 | if _, err := rand.Read(appKey[:]); err != nil { 407 | log.Fatal(err) 408 | } 409 | 410 | _, err := as.Device().Create(context.Background(), &api.CreateDeviceRequest{ 411 | Device: &api.Device{ 412 | DevEui: devEUI.String(), 413 | Name: devEUI.String(), 414 | Description: devEUI.String(), 415 | ApplicationId: s.applicationID, 416 | DeviceProfileId: s.deviceProfileID.String(), 417 | }, 418 | }) 419 | if err != nil { 420 | log.Fatal("create device error, error: %s", err) 421 | } 422 | 423 | _, err = as.Device().CreateKeys(context.Background(), &api.CreateDeviceKeysRequest{ 424 | DeviceKeys: &api.DeviceKeys{ 425 | DevEui: devEUI.String(), 426 | 427 | // yes, this is correct for LoRaWAN 1.0.x! 428 | // see the API documentation 429 | NwkKey: appKey.String(), 430 | }, 431 | }) 432 | if err != nil { 433 | log.Fatal("create device keys error, error: %s", err) 434 | } 435 | 436 | s.deviceAppKeysMutex.Lock() 437 | s.deviceAppKeys[devEUI] = appKey 438 | s.deviceAppKeysMutex.Unlock() 439 | wg.Done() 440 | }() 441 | 442 | } 443 | 444 | wg.Wait() 445 | 446 | return nil 447 | } 448 | 449 | func (s *simulation) tearDownDevices() error { 450 | log.Info("simulator: tear-down devices") 451 | 452 | for k := range s.deviceAppKeys { 453 | _, err := as.Device().Delete(context.Background(), &api.DeleteDeviceRequest{ 454 | DevEui: k.String(), 455 | }) 456 | if err != nil { 457 | return errors.Wrap(err, "delete device error") 458 | } 459 | } 460 | 461 | return nil 462 | } 463 | 464 | func (s *simulation) setupApplicationIntegration() error { 465 | log.Info("simulator: setting up application integration") 466 | 467 | token := as.MQTTClient().Subscribe(fmt.Sprintf("application/%s/device/+/event/up", s.applicationID), 0, func(client mqtt.Client, msg mqtt.Message) { 468 | applicationUplinkCounter().Inc() 469 | }) 470 | token.Wait() 471 | if token.Error() != nil { 472 | return errors.Wrap(token.Error(), "subscribe application integration error") 473 | } 474 | 475 | return nil 476 | } 477 | 478 | func (s *simulation) tearDownApplicationIntegration() error { 479 | log.Info("simulator: tear-down application integration") 480 | 481 | token := as.MQTTClient().Unsubscribe(fmt.Sprintf("application/%s/device/+/event/up", s.applicationID)) 482 | token.Wait() 483 | if token.Error() != nil { 484 | return errors.Wrap(token.Error(), "unsubscribe application integration error") 485 | } 486 | 487 | return nil 488 | } 489 | -------------------------------------------------------------------------------- /simulator/device.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "context" 5 | crand "crypto/rand" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | log "github.com/sirupsen/logrus" 14 | 15 | "github.com/brocaar/lorawan" 16 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 17 | ) 18 | 19 | // DeviceOption is the interface for a device option. 20 | type DeviceOption func(*Device) error 21 | 22 | type deviceState int 23 | 24 | const ( 25 | deviceStateOTAA deviceState = iota 26 | deviceStateActivated 27 | ) 28 | 29 | // Device contains the state of a simulated LoRaWAN OTAA device (1.0.x). 30 | type Device struct { 31 | sync.RWMutex 32 | 33 | // Context to cancel device. 34 | ctx context.Context 35 | 36 | // Cancel function. 37 | cancel context.CancelFunc 38 | 39 | // Waitgroup to wait until simulation has been fully cancelled. 40 | wg *sync.WaitGroup 41 | 42 | // DevEUI. 43 | devEUI lorawan.EUI64 44 | 45 | // JoinEUI. 46 | joinEUI lorawan.EUI64 47 | 48 | // AppKey. 49 | appKey lorawan.AES128Key 50 | 51 | // Interval in which device sends uplinks. 52 | uplinkInterval time.Duration 53 | 54 | // Total number of uplinks to send, before terminating. 55 | uplinkCount uint32 56 | 57 | // Device sends uplink as confirmed. 58 | confirmed bool 59 | 60 | // Payload (plaintext) which the device sends as uplink. 61 | payload []byte 62 | 63 | // FPort used for sending uplinks. 64 | fPort uint8 65 | 66 | // Assigned device address. 67 | devAddr lorawan.DevAddr 68 | 69 | // DevNonce. 70 | devNonce lorawan.DevNonce 71 | 72 | // Uplink frame-counter. 73 | fCntUp uint32 74 | 75 | // Downlink frame-counter. 76 | fCntDown uint32 77 | 78 | // Application session-key. 79 | appSKey lorawan.AES128Key 80 | 81 | // Network session-key. 82 | nwkSKey lorawan.AES128Key 83 | 84 | // Activation state. 85 | state deviceState 86 | 87 | // Downlink frames channel (used by the gateway). Note that the gateway 88 | // forwards downlink frames to all associated devices, as only the device 89 | // is able to validate the addressee. 90 | downlinkFrames chan gw.DownlinkFrame 91 | 92 | // The associated gateway through which the device simulates its uplinks. 93 | gateways []*Gateway 94 | 95 | // Random DevNonce 96 | randomDevNonce bool 97 | 98 | // TXInfo for uplink 99 | uplinkTXInfo gw.UplinkTxInfo 100 | 101 | // Downlink handler function. 102 | downlinkHandlerFunc func(confirmed, ack bool, fCntDown uint32, fPort uint8, data []byte) error 103 | 104 | // OTAA delay. 105 | otaaDelay time.Duration 106 | } 107 | 108 | // WithAppKey sets the AppKey. 109 | func WithAppKey(appKey lorawan.AES128Key) DeviceOption { 110 | return func(d *Device) error { 111 | d.appKey = appKey 112 | return nil 113 | } 114 | } 115 | 116 | // WithDevEUI sets the DevEUI. 117 | func WithDevEUI(devEUI lorawan.EUI64) DeviceOption { 118 | return func(d *Device) error { 119 | d.devEUI = devEUI 120 | return nil 121 | } 122 | } 123 | 124 | // WithJoinEUI sets the JoinEUI. 125 | func WithJoinEUI(joinEUI lorawan.EUI64) DeviceOption { 126 | return func(d *Device) error { 127 | d.joinEUI = joinEUI 128 | return nil 129 | } 130 | } 131 | 132 | // WithOTAADelay sets the OTAA delay. 133 | func WithOTAADelay(delay time.Duration) DeviceOption { 134 | return func(d *Device) error { 135 | d.otaaDelay = delay 136 | return nil 137 | } 138 | } 139 | 140 | // WithUplinkInterval sets the uplink interval. 141 | func WithUplinkInterval(interval time.Duration) DeviceOption { 142 | return func(d *Device) error { 143 | d.uplinkInterval = interval 144 | return nil 145 | } 146 | } 147 | 148 | // WithUplinkCount sets the uplink count, after which the device simulation 149 | // ends. 150 | func WithUplinkCount(count uint32) DeviceOption { 151 | return func(d *Device) error { 152 | d.uplinkCount = count 153 | return nil 154 | } 155 | } 156 | 157 | // WithUplinkPayload sets the uplink payload. 158 | func WithUplinkPayload(confirmed bool, fPort uint8, pl []byte) DeviceOption { 159 | return func(d *Device) error { 160 | d.fPort = fPort 161 | d.payload = pl 162 | d.confirmed = confirmed 163 | return nil 164 | } 165 | } 166 | 167 | // WithGateways adds the device to the given gateways. 168 | // Use this function after WithDevEUI! 169 | func WithGateways(gws []*Gateway) DeviceOption { 170 | return func(d *Device) error { 171 | d.gateways = gws 172 | 173 | for i := range d.gateways { 174 | d.gateways[i].addDevice(d.devEUI, d.downlinkFrames) 175 | } 176 | return nil 177 | } 178 | } 179 | 180 | // WithRandomDevNonce randomizes the OTAA DevNonce instead of using a counter value. 181 | func WithRandomDevNonce() DeviceOption { 182 | return func(d *Device) error { 183 | d.randomDevNonce = true 184 | return nil 185 | } 186 | } 187 | 188 | // WithUplinkTXInfo sets the TXInfo used for simulating the uplinks. 189 | func WithUplinkTXInfo(txInfo gw.UplinkTxInfo) DeviceOption { 190 | return func(d *Device) error { 191 | d.uplinkTXInfo = txInfo 192 | return nil 193 | } 194 | } 195 | 196 | // WithDownlinkHandlerFunc sets the downlink handler func. 197 | func WithDownlinkHandlerFunc(f func(confirmed, ack bool, fCntDown uint32, fPort uint8, data []byte) error) DeviceOption { 198 | return func(d *Device) error { 199 | d.downlinkHandlerFunc = f 200 | return nil 201 | } 202 | } 203 | 204 | // NewDevice creates a new device simulation. 205 | func NewDevice(ctx context.Context, wg *sync.WaitGroup, opts ...DeviceOption) (*Device, error) { 206 | ctx, cancel := context.WithCancel(ctx) 207 | 208 | d := &Device{ 209 | ctx: ctx, 210 | cancel: cancel, 211 | wg: wg, 212 | 213 | downlinkFrames: make(chan gw.DownlinkFrame, 100), 214 | state: deviceStateOTAA, 215 | } 216 | 217 | for _, o := range opts { 218 | if err := o(d); err != nil { 219 | return nil, err 220 | } 221 | } 222 | 223 | log.WithFields(log.Fields{ 224 | "dev_eui": d.devEUI, 225 | }).Info("simulator: new otaa device") 226 | 227 | wg.Add(2) 228 | 229 | go d.uplinkLoop() 230 | go d.downlinkLoop() 231 | 232 | return d, nil 233 | } 234 | 235 | // uplinkLoop first handle the OTAA activation, after which it will periodically 236 | // sends an uplink with the configured payload and fport. 237 | func (d *Device) uplinkLoop() { 238 | defer d.cancel() 239 | defer d.wg.Done() 240 | 241 | var cancelled bool 242 | go func() { 243 | <-d.ctx.Done() 244 | cancelled = true 245 | }() 246 | 247 | time.Sleep(d.otaaDelay) 248 | 249 | for !cancelled { 250 | switch d.getState() { 251 | case deviceStateOTAA: 252 | d.joinRequest() 253 | time.Sleep(6 * time.Second) 254 | case deviceStateActivated: 255 | d.dataUp() 256 | 257 | if d.uplinkCount != 0 { 258 | if d.fCntUp >= d.uplinkCount { 259 | // d.cancel() also cancels the downlink loop. Wait one 260 | // second in order to process any potential downlink 261 | // response (e.g. and ack). 262 | time.Sleep(time.Second) 263 | d.cancel() 264 | return 265 | } 266 | } 267 | 268 | time.Sleep(d.uplinkInterval) 269 | } 270 | } 271 | } 272 | 273 | // downlinkLoop handles the downlink messages. 274 | // Note: as a gateway does not know the addressee of the downlink, it is up to 275 | // the handling functions to validate the MIC etc.. 276 | func (d *Device) downlinkLoop() { 277 | defer d.cancel() 278 | defer d.wg.Done() 279 | 280 | for { 281 | select { 282 | case <-d.ctx.Done(): 283 | return 284 | 285 | case pl := <-d.downlinkFrames: 286 | for _, item := range pl.Items { 287 | err := func() error { 288 | var phy lorawan.PHYPayload 289 | 290 | if err := phy.UnmarshalBinary(item.PhyPayload); err != nil { 291 | return errors.Wrap(err, "unmarshal phypayload error") 292 | } 293 | 294 | switch phy.MHDR.MType { 295 | case lorawan.JoinAccept: 296 | return d.joinAccept(phy) 297 | case lorawan.UnconfirmedDataDown, lorawan.ConfirmedDataDown: 298 | return d.downlinkData(phy) 299 | } 300 | 301 | return nil 302 | }() 303 | 304 | if err != nil { 305 | log.WithError(err).Error("simulator: handle downlink frame error") 306 | } 307 | 308 | break 309 | } 310 | } 311 | } 312 | } 313 | 314 | // joinRequest sends the join-request. 315 | func (d *Device) joinRequest() { 316 | log.WithFields(log.Fields{ 317 | "dev_eui": d.devEUI, 318 | }).Debug("simulator: send OTAA request") 319 | 320 | phy := lorawan.PHYPayload{ 321 | MHDR: lorawan.MHDR{ 322 | MType: lorawan.JoinRequest, 323 | Major: lorawan.LoRaWANR1, 324 | }, 325 | MACPayload: &lorawan.JoinRequestPayload{ 326 | DevEUI: d.devEUI, 327 | JoinEUI: d.joinEUI, 328 | DevNonce: d.getDevNonce(), 329 | }, 330 | } 331 | 332 | if err := phy.SetUplinkJoinMIC(d.appKey); err != nil { 333 | log.WithError(err).Error("simulator: set uplink join mic error") 334 | return 335 | } 336 | 337 | d.sendUplink(phy) 338 | 339 | deviceJoinRequestCounter().Inc() 340 | } 341 | 342 | // dataUp sends an data uplink. 343 | func (d *Device) dataUp() { 344 | log.WithFields(log.Fields{ 345 | "dev_eui": d.devEUI, 346 | "dev_addr": d.devAddr, 347 | "confirmed": d.confirmed, 348 | }).Debug("simulator: send uplink data") 349 | 350 | mType := lorawan.UnconfirmedDataUp 351 | if d.confirmed { 352 | mType = lorawan.ConfirmedDataUp 353 | } 354 | 355 | phy := lorawan.PHYPayload{ 356 | MHDR: lorawan.MHDR{ 357 | MType: mType, 358 | Major: lorawan.LoRaWANR1, 359 | }, 360 | MACPayload: &lorawan.MACPayload{ 361 | FHDR: lorawan.FHDR{ 362 | DevAddr: d.devAddr, 363 | FCnt: d.fCntUp, 364 | FCtrl: lorawan.FCtrl{ 365 | ADR: false, 366 | }, 367 | }, 368 | FPort: &d.fPort, 369 | FRMPayload: []lorawan.Payload{ 370 | &lorawan.DataPayload{ 371 | Bytes: d.payload, 372 | }, 373 | }, 374 | }, 375 | } 376 | 377 | if err := phy.EncryptFRMPayload(d.appSKey); err != nil { 378 | log.WithError(err).Error("simulator: encrypt FRMPayload error") 379 | return 380 | } 381 | 382 | if err := phy.SetUplinkDataMIC(lorawan.LoRaWAN1_0, 0, 0, 0, d.nwkSKey, d.nwkSKey); err != nil { 383 | log.WithError(err).Error("simulator: set uplink data mic error") 384 | return 385 | } 386 | 387 | d.fCntUp++ 388 | 389 | d.sendUplink(phy) 390 | 391 | deviceUplinkCounter().Inc() 392 | } 393 | 394 | // joinAccept validates and handles the join-accept downlink. 395 | func (d *Device) joinAccept(phy lorawan.PHYPayload) error { 396 | err := phy.DecryptJoinAcceptPayload(d.appKey) 397 | if err != nil { 398 | return errors.Wrap(err, "decrypt join-accept payload error") 399 | } 400 | 401 | ok, err := phy.ValidateDownlinkJoinMIC(lorawan.JoinRequestType, d.joinEUI, d.devNonce, d.appKey) 402 | if err != nil { 403 | log.WithFields(log.Fields{ 404 | "dev_eui": d.devEUI, 405 | }).Debug("simulator: invalid join-accept MIC") 406 | return nil 407 | } 408 | if !ok { 409 | log.WithFields(log.Fields{ 410 | "dev_eui": d.devEUI, 411 | }).Debug("simulator: invalid join-accept MIC") 412 | return nil 413 | } 414 | 415 | jaPL, ok := phy.MACPayload.(*lorawan.JoinAcceptPayload) 416 | if !ok { 417 | return errors.New("expected *lorawan.JoinAcceptPayload") 418 | } 419 | 420 | d.appSKey, err = getAppSKey(jaPL.DLSettings.OptNeg, d.appKey, jaPL.HomeNetID, d.joinEUI, jaPL.JoinNonce, d.devNonce) 421 | if err != nil { 422 | return errors.Wrap(err, "get AppSKey error") 423 | } 424 | 425 | d.nwkSKey, err = getFNwkSIntKey(jaPL.DLSettings.OptNeg, d.appKey, jaPL.HomeNetID, d.joinEUI, jaPL.JoinNonce, d.devNonce) 426 | if err != nil { 427 | return errors.Wrap(err, "get NwkSKey error") 428 | } 429 | 430 | d.devAddr = jaPL.DevAddr 431 | 432 | log.WithFields(log.Fields{ 433 | "dev_eui": d.devEUI, 434 | "dev_addr": d.devAddr, 435 | }).Info("simulator: device OTAA activated") 436 | 437 | d.setState(deviceStateActivated) 438 | deviceJoinAcceptCounter().Inc() 439 | 440 | return nil 441 | } 442 | 443 | // downlinkData validates and handles the downlink data. 444 | func (d *Device) downlinkData(phy lorawan.PHYPayload) error { 445 | ok, err := phy.ValidateDownlinkDataMIC(lorawan.LoRaWAN1_0, 0, d.nwkSKey) 446 | if err != nil { 447 | log.WithFields(log.Fields{ 448 | "dev_eui": d.devEUI, 449 | }).Debug("simulator: invalid downlink data MIC") 450 | return nil 451 | } 452 | 453 | if !ok { 454 | log.WithFields(log.Fields{ 455 | "dev_eui": d.devEUI, 456 | }).Debug("simulator: invalid downlink data MIC") 457 | return nil 458 | } 459 | 460 | macPL, ok := phy.MACPayload.(*lorawan.MACPayload) 461 | if !ok { 462 | return fmt.Errorf("expected *lorawan.MACPayload, got: %T", phy.MACPayload) 463 | } 464 | 465 | gap := uint32(uint16(macPL.FHDR.FCnt) - uint16(d.fCntDown%(1<<16))) 466 | d.fCntDown = d.fCntDown + gap 467 | 468 | var data []byte 469 | var fPort uint8 470 | if macPL.FPort != nil { 471 | fPort = *macPL.FPort 472 | } 473 | 474 | if fPort != 0 { 475 | err := phy.DecryptFRMPayload(d.appSKey) 476 | if err != nil { 477 | return errors.Wrap(err, "decrypt frmpayload error") 478 | } 479 | 480 | if len(macPL.FRMPayload) != 0 { 481 | pl, ok := macPL.FRMPayload[0].(*lorawan.DataPayload) 482 | if !ok { 483 | return fmt.Errorf("expected *lorawan.DataPayload, got: %T", macPL.FRMPayload[0]) 484 | } 485 | 486 | data = pl.Bytes 487 | } 488 | } 489 | 490 | log.WithFields(log.Fields{ 491 | "confirmed": phy.MHDR.MType == lorawan.ConfirmedDataDown, 492 | "ack": macPL.FHDR.FCtrl.ACK, 493 | "f_cnt": d.fCntDown, 494 | "dev_eui": d.devEUI, 495 | "f_port": fPort, 496 | "data": hex.EncodeToString(data), 497 | }).Info("simulator: device received downlink data") 498 | 499 | if d.downlinkHandlerFunc == nil { 500 | return nil 501 | } 502 | 503 | return d.downlinkHandlerFunc(phy.MHDR.MType == lorawan.ConfirmedDataDown, macPL.FHDR.FCtrl.ACK, d.fCntDown, fPort, data) 504 | } 505 | 506 | // sendUplink sends 507 | func (d *Device) sendUplink(phy lorawan.PHYPayload) error { 508 | b, err := phy.MarshalBinary() 509 | if err != nil { 510 | return errors.Wrap(err, "marshal phypayload error") 511 | } 512 | 513 | pl := gw.UplinkFrame{ 514 | PhyPayload: b, 515 | TxInfo: &d.uplinkTXInfo, 516 | } 517 | 518 | for i := range d.gateways { 519 | if err := d.gateways[i].SendUplinkFrame(pl); err != nil { 520 | log.WithError(err).WithFields(log.Fields{ 521 | "dev_eui": d.devEUI, 522 | }).Error("simulator: send uplink frame error") 523 | } 524 | } 525 | 526 | return nil 527 | } 528 | 529 | // getDevNonce increments and returns a LoRaWAN DevNonce. 530 | func (d *Device) getDevNonce() lorawan.DevNonce { 531 | if d.randomDevNonce { 532 | b := make([]byte, 2) 533 | _, _ = crand.Read(b) 534 | 535 | d.devNonce = lorawan.DevNonce(binary.BigEndian.Uint16(b)) 536 | } else { 537 | d.devNonce++ 538 | } 539 | 540 | return d.devNonce 541 | } 542 | 543 | // getState returns the current device state. 544 | func (d *Device) getState() deviceState { 545 | d.RLock() 546 | defer d.RUnlock() 547 | 548 | return d.state 549 | } 550 | 551 | // setState sets the device to the given state. 552 | func (d *Device) setState(s deviceState) { 553 | d.Lock() 554 | d.Unlock() 555 | 556 | d.state = s 557 | } 558 | -------------------------------------------------------------------------------- /simulator/gateway.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "io/ioutil" 8 | "math/rand" 9 | "sync" 10 | "text/template" 11 | "time" 12 | 13 | mqtt "github.com/eclipse/paho.mqtt.golang" 14 | "github.com/golang/protobuf/proto" 15 | "github.com/pkg/errors" 16 | log "github.com/sirupsen/logrus" 17 | 18 | "github.com/brocaar/lorawan" 19 | "github.com/chirpstack/chirpstack/api/go/v4/gw" 20 | ) 21 | 22 | // GatewayOption is the interface for a gateway option. 23 | type GatewayOption func(*Gateway) error 24 | 25 | // Gateway defines a simulated LoRa gateway. 26 | type Gateway struct { 27 | mqtt mqtt.Client 28 | gatewayID lorawan.EUI64 29 | 30 | deviceMux sync.RWMutex 31 | devices map[lorawan.EUI64]chan gw.DownlinkFrame 32 | 33 | downlinkTxNAckRate int 34 | downlinkTxCounter int 35 | downlinkTxAckDelay time.Duration 36 | 37 | eventTopicTemplate *template.Template 38 | commandTopicTemplate *template.Template 39 | } 40 | 41 | // WithMQTTClient sets the MQTT client for the gateway. 42 | func WithMQTTClient(client mqtt.Client) GatewayOption { 43 | return func(g *Gateway) error { 44 | g.mqtt = client 45 | return nil 46 | } 47 | } 48 | 49 | // WithMQTTCredentials initializes a new MQTT client with the given credentials. 50 | func WithMQTTCredentials(server, username, password string) GatewayOption { 51 | return func(g *Gateway) error { 52 | opts := mqtt.NewClientOptions() 53 | opts.AddBroker(server) 54 | opts.SetUsername(username) 55 | opts.SetPassword(password) 56 | opts.SetCleanSession(true) 57 | opts.SetAutoReconnect(true) 58 | 59 | log.WithFields(log.Fields{ 60 | "server": server, 61 | }).Info("simulator: connecting to mqtt broker") 62 | 63 | client := mqtt.NewClient(opts) 64 | if token := client.Connect(); token.Wait() && token.Error() != nil { 65 | return errors.Wrap(token.Error(), "mqtt client connect error") 66 | } 67 | 68 | g.mqtt = client 69 | 70 | return nil 71 | } 72 | } 73 | 74 | // WithMQTTCertificates initializes a new MQTT client with the given CA and 75 | // client-certificate files. 76 | func WithMQTTCertificates(server, caCert, tlsCert, tlsKey string) GatewayOption { 77 | return func(g *Gateway) error { 78 | tlsConfig := &tls.Config{} 79 | 80 | if caCert != "" { 81 | b, err := ioutil.ReadFile(caCert) 82 | if err != nil { 83 | return errors.Wrap(err, "read ca certificate error") 84 | } 85 | 86 | certpool := x509.NewCertPool() 87 | certpool.AppendCertsFromPEM(b) 88 | tlsConfig.RootCAs = certpool 89 | } 90 | 91 | if tlsCert != "" && tlsKey != "" { 92 | kp, err := tls.LoadX509KeyPair(tlsCert, tlsKey) 93 | if err != nil { 94 | return errors.Wrap(err, "read tls key-pair error") 95 | } 96 | 97 | tlsConfig.Certificates = []tls.Certificate{kp} 98 | } 99 | 100 | opts := mqtt.NewClientOptions() 101 | opts.AddBroker(server) 102 | opts.SetCleanSession(true) 103 | opts.SetAutoReconnect(true) 104 | opts.SetTLSConfig(tlsConfig) 105 | 106 | log.WithFields(log.Fields{ 107 | "ca_cert": caCert, 108 | "tls_cert": tlsCert, 109 | "tls_key": tlsKey, 110 | }).Info("simulator: connecting to mqtt broker") 111 | 112 | client := mqtt.NewClient(opts) 113 | if token := client.Connect(); token.Wait() && token.Error() != nil { 114 | return errors.Wrap(token.Error(), "mqtt client connect error") 115 | } 116 | 117 | g.mqtt = client 118 | 119 | return nil 120 | } 121 | } 122 | 123 | // WithGatewayID sets the gateway ID. 124 | func WithGatewayID(gatewayID lorawan.EUI64) GatewayOption { 125 | return func(g *Gateway) error { 126 | g.gatewayID = gatewayID 127 | return nil 128 | } 129 | } 130 | 131 | // WithDownlinkTxNackRate sets the rate in which Tx NAck messages are sent. 132 | // Setting this to: 133 | // 0: always ACK 134 | // 1: NAck every message 135 | // 2: NAck every other message 136 | // 3: NAck every third message 137 | // ... 138 | func WithDownlinkTxNackRate(rate int) GatewayOption { 139 | return func(g *Gateway) error { 140 | g.downlinkTxNAckRate = rate 141 | return nil 142 | } 143 | } 144 | 145 | // WithDownlinkTxAckDelay sets the delay in which the Tx Ack is returned. 146 | func WithDownlinkTxAckDelay(d time.Duration) GatewayOption { 147 | return func(g *Gateway) error { 148 | g.downlinkTxAckDelay = d 149 | return nil 150 | } 151 | } 152 | 153 | // WithEventTopicTemplate sets the event (gw > ns) topic template. 154 | // Example: 'gateway/{{ .GatewayID }}/event/{{ .Event }}' 155 | func WithEventTopicTemplate(tt string) GatewayOption { 156 | return func(g *Gateway) error { 157 | var err error 158 | g.eventTopicTemplate, err = template.New("event").Parse(tt) 159 | if err != nil { 160 | return errors.Wrap(err, "parse event topic template error") 161 | } 162 | 163 | return nil 164 | } 165 | } 166 | 167 | // WithCommandTopicTemplate sets the command (ns > gw) topic template. 168 | // Example: 'gateway/{{ .GatewayID }}/command/{{ .Command }}' 169 | func WithCommandTopicTemplate(ct string) GatewayOption { 170 | return func(g *Gateway) error { 171 | var err error 172 | g.commandTopicTemplate, err = template.New("command").Parse(ct) 173 | if err != nil { 174 | return errors.Wrap(err, "parse command topic template error") 175 | } 176 | 177 | return nil 178 | } 179 | } 180 | 181 | // NewGateway creates a new gateway, using the given MQTT client for sending 182 | // and receiving. 183 | func NewGateway(opts ...GatewayOption) (*Gateway, error) { 184 | gw := &Gateway{ 185 | devices: make(map[lorawan.EUI64]chan gw.DownlinkFrame), 186 | } 187 | 188 | for _, o := range opts { 189 | if err := o(gw); err != nil { 190 | return nil, err 191 | } 192 | } 193 | 194 | downlinkTopic := gw.getCommandTopic("down") 195 | 196 | log.WithFields(log.Fields{ 197 | "gateway_id": gw.gatewayID, 198 | "topic": downlinkTopic, 199 | }).Info("simulator: subscribing to gateway mqtt topic") 200 | for { 201 | if token := gw.mqtt.Subscribe(downlinkTopic, 0, gw.downlinkEventHandler); token.Wait() && token.Error() != nil { 202 | log.WithError(token.Error()).WithFields(log.Fields{ 203 | "gateway_id": gw.gatewayID, 204 | "topic": downlinkTopic, 205 | }).Error("simulator: subscribe to mqtt topic error") 206 | time.Sleep(time.Second * 2) 207 | } else { 208 | break 209 | } 210 | } 211 | 212 | return gw, nil 213 | } 214 | 215 | // SendUplinkFrame sends the given uplink frame. 216 | func (g *Gateway) SendUplinkFrame(pl gw.UplinkFrame) error { 217 | pl.RxInfo = &gw.UplinkRxInfo{ 218 | GatewayId: g.gatewayID.String(), 219 | Rssi: 50, 220 | Snr: 5.5, 221 | Context: []byte{0x01, 0x02, 0x03, 0x04}, 222 | UplinkId: rand.Uint32(), 223 | } 224 | 225 | b, err := proto.Marshal(&pl) 226 | if err != nil { 227 | return errors.Wrap(err, "send uplink frame error") 228 | } 229 | 230 | uplinkTopic := g.getEventTopic("up") 231 | 232 | log.WithFields(log.Fields{ 233 | "gateway_id": g.gatewayID, 234 | "topic": uplinkTopic, 235 | }).Debug("simulator: publish uplink frame") 236 | 237 | if token := g.mqtt.Publish(uplinkTopic, 0, false, b); token.Wait() && token.Error() != nil { 238 | return errors.Wrap(err, "simulator: publish uplink frame error") 239 | } 240 | 241 | gatewayUplinkCounter().Inc() 242 | 243 | return nil 244 | } 245 | 246 | // sendDownlinkTxAck sends the given downlink Ack. 247 | func (g *Gateway) sendDownlinkTxAck(pl gw.DownlinkTxAck) error { 248 | b, err := proto.Marshal(&pl) 249 | if err != nil { 250 | return errors.Wrap(err, "send tx ack error") 251 | } 252 | 253 | ackTopic := g.getEventTopic("ack") 254 | 255 | log.WithFields(log.Fields{ 256 | "gateway_id": g.gatewayID, 257 | "topic": ackTopic, 258 | }).Debug("simulator: publish downlink tx ack") 259 | 260 | if token := g.mqtt.Publish(ackTopic, 0, false, b); token.Wait() && token.Error() != nil { 261 | return errors.Wrap(err, "simulator: publish downlink tx ack error") 262 | } 263 | 264 | return nil 265 | } 266 | 267 | // addDevice adds the given device to the 'coverage' of the gateway. 268 | // This means that any downlink sent to the gateway will be forwarded to added 269 | // devices (which will each validate the DevAddr and MIC). 270 | func (g *Gateway) addDevice(devEUI lorawan.EUI64, c chan gw.DownlinkFrame) { 271 | g.deviceMux.Lock() 272 | defer g.deviceMux.Unlock() 273 | 274 | log.WithFields(log.Fields{ 275 | "dev_eui": devEUI, 276 | "gateway_id": g.gatewayID, 277 | }).Info("simulator: add device to gateway") 278 | 279 | g.devices[devEUI] = c 280 | } 281 | 282 | func (g *Gateway) getEventTopic(event string) string { 283 | topic := bytes.NewBuffer(nil) 284 | 285 | err := g.eventTopicTemplate.Execute(topic, struct { 286 | GatewayID lorawan.EUI64 287 | Event string 288 | }{g.gatewayID, event}) 289 | if err != nil { 290 | log.WithError(err).Fatal("execute event topic template error") 291 | } 292 | 293 | return topic.String() 294 | } 295 | 296 | func (g *Gateway) getCommandTopic(command string) string { 297 | topic := bytes.NewBuffer(nil) 298 | 299 | err := g.commandTopicTemplate.Execute(topic, struct { 300 | GatewayID lorawan.EUI64 301 | Command string 302 | }{g.gatewayID, command}) 303 | if err != nil { 304 | log.WithError(err).Fatal("execute command topic template error") 305 | } 306 | 307 | return topic.String() 308 | } 309 | 310 | func (g *Gateway) downlinkEventHandler(c mqtt.Client, msg mqtt.Message) { 311 | g.deviceMux.RLock() 312 | defer g.deviceMux.RUnlock() 313 | 314 | log.WithFields(log.Fields{ 315 | "gateway_id": g.gatewayID, 316 | "topic": msg.Topic(), 317 | }).Debug("simulator: downlink command received") 318 | 319 | gatewayDownlinkCounter().Inc() 320 | 321 | var pl gw.DownlinkFrame 322 | if err := proto.Unmarshal(msg.Payload(), &pl); err != nil { 323 | log.WithError(err).Error("simulator: unmarshal downlink command error") 324 | } 325 | 326 | for devEUI, downChan := range g.devices { 327 | log.WithFields(log.Fields{ 328 | "dev_eui": devEUI, 329 | "gateway_id": g.gatewayID, 330 | }).Debug("simulator: forwarding downlink to device") 331 | downChan <- pl 332 | } 333 | 334 | time.Sleep(g.downlinkTxAckDelay) 335 | 336 | items := []*gw.DownlinkTxAckItem{} 337 | 338 | g.downlinkTxCounter++ 339 | if g.downlinkTxCounter == g.downlinkTxNAckRate { 340 | g.downlinkTxCounter = 0 341 | 342 | for range pl.Items { 343 | items = append(items, &gw.DownlinkTxAckItem{ 344 | Status: gw.TxAckStatus_COLLISION_PACKET, 345 | }) 346 | } 347 | 348 | } else { 349 | for range pl.Items { 350 | items = append(items, &gw.DownlinkTxAckItem{ 351 | Status: gw.TxAckStatus_OK, 352 | }) 353 | } 354 | } 355 | 356 | txNack := gw.DownlinkTxAck{ 357 | GatewayId: g.gatewayID.String(), 358 | DownlinkId: pl.DownlinkId, 359 | Items: items, 360 | } 361 | 362 | if err := g.sendDownlinkTxAck(txNack); err != nil { 363 | log.WithError(err).WithFields(log.Fields{ 364 | "gateway_id": g.gatewayID, 365 | }).Error("simulator: send downlink tx ack error") 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /simulator/metrics.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | duc = promauto.NewCounter(prometheus.CounterOpts{ 10 | Name: "device_uplink_count", 11 | Help: "The number of uplinks sent by the devices.", 12 | }) 13 | 14 | djrc = promauto.NewCounter(prometheus.CounterOpts{ 15 | Name: "device_join_request_count", 16 | Help: "The number of join-requests sent by the devices.", 17 | }) 18 | 19 | djac = promauto.NewCounter(prometheus.CounterOpts{ 20 | Name: "device_join_accept_count", 21 | Help: "The number of join-accepts received by the devices.", 22 | }) 23 | 24 | guc = promauto.NewCounter(prometheus.CounterOpts{ 25 | Name: "gateway_uplink_count", 26 | Help: "The number of uplinks sent by the gateways.", 27 | }) 28 | 29 | gdc = promauto.NewCounter(prometheus.CounterOpts{ 30 | Name: "gateway_downlink_count", 31 | Help: "The number of downlinks received by the gateways.", 32 | }) 33 | ) 34 | 35 | func deviceUplinkCounter() prometheus.Counter { 36 | return duc 37 | } 38 | 39 | func deviceJoinRequestCounter() prometheus.Counter { 40 | return djrc 41 | } 42 | 43 | func deviceJoinAcceptCounter() prometheus.Counter { 44 | return djac 45 | } 46 | 47 | func gatewayUplinkCounter() prometheus.Counter { 48 | return guc 49 | } 50 | 51 | func gatewayDownlinkCounter() prometheus.Counter { 52 | return gdc 53 | } 54 | -------------------------------------------------------------------------------- /simulator/session_keys.go: -------------------------------------------------------------------------------- 1 | package simulator 2 | 3 | import ( 4 | "crypto/aes" 5 | "fmt" 6 | 7 | "github.com/brocaar/lorawan" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // getAppSKey returns appSKey. 12 | func getAppSKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 13 | return getSKey(optNeg, 0x02, nwkKey, netID, joinEUI, joinNonce, devNonce) 14 | } 15 | 16 | // getFNwkSIntKey returns the FNwkSIntKey. 17 | // For LoRaWAN 1.0: SNwkSIntKey = NwkSEncKey = FNwkSIntKey = NwkSKey 18 | func getFNwkSIntKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 19 | return getSKey(optNeg, 0x01, nwkKey, netID, joinEUI, joinNonce, devNonce) 20 | } 21 | 22 | func getSKey(optNeg bool, typ byte, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 23 | var key lorawan.AES128Key 24 | b := make([]byte, 16) 25 | b[0] = typ 26 | 27 | netIDB, err := netID.MarshalBinary() 28 | if err != nil { 29 | return key, errors.Wrap(err, "marshal binary error") 30 | } 31 | 32 | joinEUIB, err := joinEUI.MarshalBinary() 33 | if err != nil { 34 | return key, errors.Wrap(err, "marshal binary error") 35 | } 36 | 37 | joinNonceB, err := joinNonce.MarshalBinary() 38 | if err != nil { 39 | return key, errors.Wrap(err, "marshal binary error") 40 | } 41 | 42 | devNonceB, err := devNonce.MarshalBinary() 43 | if err != nil { 44 | return key, errors.Wrap(err, "marshal binary error") 45 | } 46 | 47 | if optNeg { 48 | copy(b[1:4], joinNonceB) 49 | copy(b[4:12], joinEUIB) 50 | copy(b[12:14], devNonceB) 51 | } else { 52 | copy(b[1:4], joinNonceB) 53 | copy(b[4:7], netIDB) 54 | copy(b[7:9], devNonceB) 55 | } 56 | 57 | block, err := aes.NewCipher(nwkKey[:]) 58 | if err != nil { 59 | return key, err 60 | } 61 | if block.BlockSize() != len(b) { 62 | return key, fmt.Errorf("block-size of %d bytes is expected", len(b)) 63 | } 64 | block.Encrypt(key[:], b) 65 | 66 | return key, nil 67 | } 68 | --------------------------------------------------------------------------------