├── .script └── gen-reference ├── .editorconfig ├── AUTHORS ├── sdk.go ├── .travis.yml ├── procedures.go ├── go.mod ├── utils.go ├── CONTRIBUTING.md ├── README.md ├── LICENSE ├── handler.go ├── simulate.go ├── discovery.go ├── .gitignore ├── application_manager_test.go ├── client_test.go ├── mocks_test.go ├── mqtt_test.go ├── application_manager.go ├── client.go ├── device_manager_test.go ├── sdk_test.go ├── mqtt.go ├── device_manager.go ├── API.md └── go.sum /.script/gen-reference: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | go get github.com/robertkrimen/godocdown/godocdown 4 | 5 | cd $GOPATH/src/github.com/TheThingsNetwork/go-app-sdk 6 | godocdown . | sed \ 7 | -e 's/--//' \ 8 | -e 's/# ttnsdk/# API Reference/' \ 9 | -e 's/import /import ttnsdk /' \ 10 | -e 's/####/##/g' \ 11 | -e 's/## Usage/## Variables/' > API.md 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of The Things Network Go SDK authors for copyright purposes. 2 | # 3 | # The copyright owners listed in this document agree to release their work under 4 | # the MIT license that can be found in the LICENSE file. 5 | # 6 | # Names should be added to this file as 7 | # Firstname Lastname 8 | # 9 | # Please keep the list sorted. 10 | 11 | Hylke Visser 12 | -------------------------------------------------------------------------------- /sdk.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | // Package ttnsdk implements the Go SDK for The Things Network. 5 | // 6 | // This package wraps The Things Network's application and device management APIs (github.com/TheThingsNetwork/api) 7 | // and the publish/subscribe API (github.com/TheThingsNetwork/ttn/mqtt). It works with the Discovery Server to retrieve 8 | // the addresses of the Handler and MQTT server. 9 | package ttnsdk 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: required 4 | 5 | services: 6 | - docker 7 | 8 | go: 9 | - '1.11.x' 10 | - '1.12.x' 11 | 12 | go_import_path: github.com/TheThingsNetwork/go-app-sdk 13 | 14 | env: 15 | global: 16 | - GOPROXY=https://proxy.golang.org 17 | - GO111MODULE=on 18 | 19 | before_install: 20 | - go get github.com/mattn/goveralls 21 | 22 | install: 23 | - go mod download 24 | 25 | before_script: 26 | - docker run -d -p 127.0.0.1:1883:1883 thethingsnetwork/rabbitmq 27 | 28 | script: 29 | - go test -v -cover . 30 | 31 | after_script: 32 | - goveralls -service=travis-ci 33 | -------------------------------------------------------------------------------- /procedures.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | // MoveDevice moves a device to another application 7 | func MoveDevice(devID string, from, to DeviceManager) (err error) { 8 | dev, err := from.Get(devID) 9 | if err != nil { 10 | return err 11 | } 12 | err = from.Delete(devID) 13 | if err != nil { 14 | return err 15 | } 16 | defer func() { 17 | if err != nil { 18 | from.Set(dev) 19 | } 20 | }() 21 | err = to.Set(dev) 22 | if err != nil { 23 | return err 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TheThingsNetwork/go-app-sdk 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/TheThingsNetwork/api v0.0.0-20190516111443-a3523f89e84f 7 | github.com/TheThingsNetwork/go-account-lib v0.0.0-20190516094738-77d15a3f8875 8 | github.com/TheThingsNetwork/go-utils v0.0.0-20190516083235-bdd4967fab4e 9 | github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516112328-fcd38e2b9dc6 10 | github.com/TheThingsNetwork/ttn/mqtt v0.0.0-20190516112328-fcd38e2b9dc6 11 | github.com/gogo/protobuf v1.2.1 12 | github.com/mwitkow/go-grpc-middleware v1.0.0 13 | github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 14 | golang.org/x/net v0.0.0-20190514140710-3ec191127204 15 | google.golang.org/grpc v1.20.1 16 | ) 17 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "fmt" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | func cleanMQTTAddress(in string) (address string, err error) { 13 | if !strings.Contains(in, "://") { 14 | in = "detect://" + in 15 | } 16 | url, err := url.Parse(in) 17 | if err != nil { 18 | return address, err 19 | } 20 | switch url.Scheme { 21 | case "detect", "mqtt", "mqtts": 22 | default: 23 | return address, fmt.Errorf("ttn-sdk: unknown mqtt scheme: %s", url.Scheme) 24 | } 25 | scheme, host, port := url.Scheme, url.Hostname(), url.Port() 26 | if scheme == "detect" { 27 | switch port { 28 | case "8883", "": 29 | scheme = "ssl" 30 | default: 31 | scheme = "tcp" 32 | } 33 | } 34 | if port == "" { 35 | switch scheme { 36 | case "ssl", "mqtts": 37 | scheme = "ssl" 38 | port = "8883" 39 | case "tcp", "mqtt": 40 | scheme = "tcp" 41 | port = "1883" 42 | } 43 | } 44 | return fmt.Sprintf("%s://%s:%s", scheme, host, port), nil 45 | } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | There are many ways you can contribute to The Things Network. 4 | 5 | - Share your work on the [Labs section of our website](https://www.thethingsnetwork.org/labs/) 6 | - Participate in [topics on our Forum](https://thethingsnetwork.org/forum/) 7 | - Talk with others on our [Slack](https://thethingsnetwork.slack.com/) ([_request invite_](https://account.thethingsnetwork.org)) 8 | - Contribute to our [open source projects on github](https://github.com/TheThingsNetwork) 9 | 10 | ## Submitting issues 11 | 12 | If something is wrong or missing, we want to know about it. Please submit an issue on Github explaining what exactly is the problem. **Give as many details as possible**. If you can (and want to) fix the issue, please tell us in the issue. 13 | 14 | ## Contributing pull requests 15 | 16 | We warmly welcome your pull requests. Be sure to follow some simple guidelines so that we can quickly accept your contributions. 17 | 18 | - Write tests 19 | - Write [good commit messages](https://chris.beams.io/posts/git-commit/) 20 | - Sign our [CLA](https://cla-assistant.io/TheThingsNetwork/go-app-sdk) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Things Network Go SDK 2 | 3 | [![Build Status](https://travis-ci.org/TheThingsNetwork/go-app-sdk.svg?branch=master)](https://travis-ci.org/TheThingsNetwork/go-app-sdk) [![Coverage Status](https://coveralls.io/repos/github/TheThingsNetwork/go-app-sdk/badge.svg?branch=master)](https://coveralls.io/github/TheThingsNetwork/go-app-sdk?branch=master) [![GoDoc](https://godoc.org/github.com/TheThingsNetwork/go-app-sdk?status.svg)](https://godoc.org/github.com/TheThingsNetwork/go-app-sdk) 4 | 5 | ![The Things Network](https://thethings.blob.core.windows.net/ttn/logo.svg) 6 | 7 | ## Usage 8 | 9 | Assuming you're working on a project `github.com/your-username/your-project`: 10 | 11 | ``` 12 | cd your-project 13 | go mod init github.com/your-username/your-project 14 | go get github.com/TheThingsNetwork/go-app-sdk 15 | ``` 16 | 17 | See the examples [on GoDoc](https://godoc.org/github.com/TheThingsNetwork/go-app-sdk#example-package). 18 | 19 | ## License 20 | 21 | Source code for The Things Network is released under the MIT License, which can be found in the [LICENSE](LICENSE) file. A list of authors can be found in the [AUTHORS](AUTHORS) file. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 The Things Network 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 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "strings" 8 | 9 | "github.com/TheThingsNetwork/go-utils/log" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials" 12 | ) 13 | 14 | func (c *client) connectHandler() (err error) { 15 | c.handler.Lock() 16 | defer c.handler.Unlock() 17 | if c.handler.conn != nil { 18 | return nil 19 | } 20 | if c.handler.announcement == nil { 21 | if err := c.discover(); err != nil { 22 | return err 23 | } 24 | } 25 | tlsConfig, err := c.handler.announcement.GetTLSConfig() 26 | if err != nil { 27 | return err 28 | } 29 | logger := c.Logger.WithFields(log.Fields{ 30 | "ID": c.handler.announcement.ID, 31 | "Address": c.handler.announcement.NetAddress, 32 | }) 33 | logger.Debug("ttn-sdk: Connecting to handler...") 34 | c.handler.conn, err = grpc.Dial( 35 | strings.Split(c.handler.announcement.NetAddress, ",")[0], 36 | append(DialOptions, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))...) 37 | if err != nil { 38 | logger.WithError(err).Debug("ttn-sdk: Could not connect to handler") 39 | return err 40 | } 41 | logger.Debug("ttn-sdk: Connected to handler") 42 | return nil 43 | } 44 | 45 | func (c *client) closeHandler() error { 46 | c.handler.Lock() 47 | defer c.handler.Unlock() 48 | if c.handler.conn != nil { 49 | c.handler.conn.Close() 50 | } 51 | c.handler.conn = nil 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /simulate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | "github.com/TheThingsNetwork/api/handler" 11 | "github.com/TheThingsNetwork/go-utils/log" 12 | ) 13 | 14 | // Simulator simulates messages for devices 15 | type Simulator interface { 16 | Uplink(port uint8, payload []byte) error 17 | } 18 | 19 | type simulator struct { 20 | logger log.Interface 21 | client handler.ApplicationManagerClient 22 | getContext func(context.Context) context.Context 23 | requestTimeout time.Duration 24 | 25 | appID string 26 | devID string 27 | } 28 | 29 | func (c *client) Simulate(devID string) (Simulator, error) { 30 | if err := c.connectHandler(); err != nil { 31 | return nil, err 32 | } 33 | return &simulator{ 34 | logger: c.Logger, 35 | client: handler.NewApplicationManagerClient(c.handler.conn), 36 | getContext: c.getContext, 37 | requestTimeout: c.RequestTimeout, 38 | appID: c.appID, 39 | devID: devID, 40 | }, nil 41 | } 42 | 43 | func (s *simulator) Uplink(port uint8, payload []byte) error { 44 | ctx, cancel := context.WithTimeout(s.getContext(context.Background()), s.requestTimeout) 45 | defer cancel() 46 | _, err := s.client.SimulateUplink(ctx, &handler.SimulatedUplinkMessage{ 47 | AppID: s.appID, 48 | DevID: s.devID, 49 | Payload: payload, 50 | Port: uint32(port), 51 | }) 52 | return err 53 | } 54 | -------------------------------------------------------------------------------- /discovery.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/TheThingsNetwork/api/discovery" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | func (c *client) discover() (err error) { 14 | logger := c.Logger.WithField("Address", c.DiscoveryServerAddress) 15 | logger.Debug("ttn-sdk: Connecting to discovery...") 16 | if c.DiscoveryServerInsecure { 17 | c.discovery.conn, err = grpc.Dial(c.DiscoveryServerAddress, append(DialOptions, grpc.WithInsecure())...) 18 | } else { 19 | c.discovery.conn, err = grpc.Dial(c.DiscoveryServerAddress, append(DialOptions, grpc.WithTransportCredentials(c.transportCredentials))...) 20 | } 21 | if err != nil { 22 | logger.WithError(err).Debug("ttn-sdk: Could not connect to discovery") 23 | return err 24 | } 25 | logger.Debug("ttn-sdk: Connected to discovery") 26 | discoveryClient := discovery.NewDiscoveryClient(c.discovery.conn) 27 | ctx, cancel := context.WithTimeout(c.getContext(context.Background()), c.RequestTimeout) 28 | defer cancel() 29 | c.Logger.Debug("ttn-sdk: Finding handler...") 30 | handler, err := discoveryClient.GetByAppID(ctx, &discovery.GetByAppIDRequest{AppID: c.appID}) 31 | if err != nil { 32 | c.Logger.WithError(err).Debug("ttn-sdk: Could not find handler for application") 33 | return err 34 | } 35 | c.handler.announcement = handler 36 | return nil 37 | } 38 | 39 | func (c *client) closeDiscovery() error { 40 | c.discovery.Lock() 41 | defer c.discovery.Unlock() 42 | if c.discovery.conn != nil { 43 | c.discovery.conn.Close() 44 | } 45 | c.discovery.conn = nil 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,linux,windows,go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 18 | .glide/ 19 | 20 | ### Linux ### 21 | *~ 22 | 23 | # temporary files which can be created if a process still has a handle open of a deleted file 24 | .fuse_hidden* 25 | 26 | # KDE directory preferences 27 | .directory 28 | 29 | # Linux trash folder which might appear on any partition or disk 30 | .Trash-* 31 | 32 | # .nfs files are created when an open file is removed but is still being accessed 33 | .nfs* 34 | 35 | ### macOS ### 36 | *.DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Icon must end with two \r 41 | Icon 42 | 43 | # Thumbnails 44 | ._* 45 | 46 | # Files that might appear in the root of a volume 47 | .DocumentRevisions-V100 48 | .fseventsd 49 | .Spotlight-V100 50 | .TemporaryItems 51 | .Trashes 52 | .VolumeIcon.icns 53 | .com.apple.timemachine.donotpresent 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | ### Windows ### 63 | # Windows thumbnail cache files 64 | Thumbs.db 65 | ehthumbs.db 66 | ehthumbs_vista.db 67 | 68 | # Folder config file 69 | Desktop.ini 70 | 71 | # Recycle Bin used on file shares 72 | $RECYCLE.BIN/ 73 | 74 | # Windows Installer files 75 | *.cab 76 | *.msi 77 | *.msm 78 | *.msp 79 | 80 | # Windows shortcuts 81 | *.lnk 82 | 83 | # End of https://www.gitignore.io/api/macos,linux,windows,go 84 | 85 | /vendor 86 | -------------------------------------------------------------------------------- /application_manager_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/api/handler" 13 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 14 | testlog "github.com/TheThingsNetwork/go-utils/log/test" 15 | . "github.com/smartystreets/assertions" 16 | ) 17 | 18 | func TestApplicationManager(t *testing.T) { 19 | a := New(t) 20 | 21 | log := testlog.NewLogger() 22 | ttnlog.Set(log) 23 | defer log.Print(t) 24 | 25 | mock := new(mockApplicationManagerClient) 26 | 27 | manager := &applicationManager{ 28 | logger: log, 29 | client: mock, 30 | getContext: func(ctx context.Context) context.Context { return ctx }, 31 | requestTimeout: time.Second, 32 | appID: "test", 33 | } 34 | 35 | someErr := errors.New("some error") 36 | 37 | { 38 | mock.reset() 39 | mock.err = someErr 40 | _, err := manager.GetPayloadFormat() 41 | a.So(err, ShouldNotBeNil) 42 | 43 | mock.reset() 44 | mock.application = &handler.Application{PayloadFormat: "custom"} 45 | pf, err := manager.GetPayloadFormat() 46 | a.So(err, ShouldBeNil) 47 | a.So(pf, ShouldEqual, "custom") 48 | 49 | err = manager.SetPayloadFormat("other") 50 | a.So(err, ShouldBeNil) 51 | a.So(mock.application.PayloadFormat, ShouldEqual, "other") 52 | 53 | mock.err = someErr 54 | err = manager.SetPayloadFormat("") 55 | a.So(err, ShouldNotBeNil) 56 | } 57 | 58 | { 59 | mock.reset() 60 | mock.err = someErr 61 | _, _, _, _, err := manager.GetCustomPayloadFunctions() 62 | a.So(err, ShouldNotBeNil) 63 | 64 | mock.reset() 65 | mock.application = &handler.Application{PayloadFormat: "other"} 66 | _, _, _, _, err = manager.GetCustomPayloadFunctions() 67 | a.So(err, ShouldNotBeNil) 68 | 69 | mock.application = &handler.Application{PayloadFormat: "custom", Decoder: "decoder", Converter: "converter", Validator: "validator", Encoder: "encoder"} 70 | jsDecoder, jsConverter, jsValidator, jsEncoder, err := manager.GetCustomPayloadFunctions() 71 | a.So(err, ShouldBeNil) 72 | a.So(jsDecoder, ShouldEqual, "decoder") 73 | a.So(jsConverter, ShouldEqual, "converter") 74 | a.So(jsValidator, ShouldEqual, "validator") 75 | a.So(jsEncoder, ShouldEqual, "encoder") 76 | 77 | err = manager.SetCustomPayloadFunctions("newdecoder", "newconverter", "newvalidator", "newencoder") 78 | a.So(err, ShouldBeNil) 79 | a.So(mock.application.PayloadFormat, ShouldEqual, "custom") 80 | a.So(mock.application.Decoder, ShouldEqual, "newdecoder") 81 | a.So(mock.application.Converter, ShouldEqual, "newconverter") 82 | a.So(mock.application.Validator, ShouldEqual, "newvalidator") 83 | a.So(mock.application.Encoder, ShouldEqual, "newencoder") 84 | 85 | mock.err = someErr 86 | err = manager.SetCustomPayloadFunctions("", "", "", "") 87 | a.So(err, ShouldNotBeNil) 88 | } 89 | 90 | // TODO: TestCustomUplinkPayloadFunctions(jsDecoder, jsConverter, jsValidator string, payload []byte, port uint8) (*handler.DryUplinkResult, error) { 91 | // TODO: TestCustomDownlinkPayloadFunctions(jsEncoder string, fields map[string]interface{}, port uint8) (*handler.DryDownlinkResult, error) { 92 | } 93 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 13 | testlog "github.com/TheThingsNetwork/go-utils/log/test" 14 | "github.com/TheThingsNetwork/go-utils/random" 15 | "github.com/TheThingsNetwork/ttn/core/types" 16 | . "github.com/smartystreets/assertions" 17 | ) 18 | 19 | func TestClient(t *testing.T) { 20 | a := New(t) 21 | 22 | for _, env := range strings.Split("APP_ID APP_ACCESS_KEY", " ") { 23 | if os.Getenv(env) == "" { 24 | t.Skipf("Skipping client test: %s not configured", env) 25 | } 26 | } 27 | 28 | log := testlog.NewLogger() 29 | ttnlog.Set(log) 30 | defer log.Print(t) 31 | 32 | appID := os.Getenv("APP_ID") 33 | 34 | config := NewCommunityConfig("client-test") 35 | client := config.NewClient(appID, os.Getenv("APP_ACCESS_KEY")).(*client) 36 | 37 | client.connectHandler() 38 | client.connectMQTT() 39 | 40 | time.Sleep(10 * time.Millisecond) 41 | 42 | client.Close() 43 | 44 | time.Sleep(time.Second) 45 | 46 | client.connectHandler() 47 | client.connectMQTT() 48 | 49 | time.Sleep(10 * time.Millisecond) 50 | 51 | devs, err := client.ManageDevices() 52 | a.So(err, ShouldBeNil) 53 | 54 | var appEUI types.AppEUI 55 | var devEUI types.DevEUI 56 | var appKey types.AppKey 57 | random.FillBytes(appEUI[:]) 58 | random.FillBytes(devEUI[:]) 59 | random.FillBytes(appKey[:]) 60 | 61 | dev := &Device{ 62 | SparseDevice: SparseDevice{ 63 | AppID: appID, 64 | DevID: "sdk-test", 65 | AppEUI: appEUI, 66 | DevEUI: devEUI, 67 | Description: "SDK Test Device", 68 | AppKey: &appKey, 69 | }, 70 | Uses32BitFCnt: true, 71 | } 72 | 73 | err = devs.Set(dev) 74 | a.So(err, ShouldBeNil) 75 | defer devs.Delete("sdk-test") 76 | 77 | deviceList, err := devs.List(0, 0) 78 | a.So(err, ShouldBeNil) 79 | a.So(deviceList, ShouldNotBeEmpty) 80 | 81 | dev, err = devs.Get("sdk-test") 82 | a.So(err, ShouldBeNil) 83 | 84 | err = dev.PersonalizeRandom() 85 | a.So(err, ShouldBeNil) 86 | a.So(dev.DevAddr, ShouldNotBeNil) 87 | 88 | pubsub, err := client.PubSub() 89 | a.So(err, ShouldBeNil) 90 | 91 | uplink, err := pubsub.AllDevices().SubscribeUplink() 92 | a.So(err, ShouldBeNil) 93 | 94 | sim, err := client.Simulate("sdk-test") 95 | a.So(err, ShouldBeNil) 96 | 97 | err = sim.Uplink(1, []byte{0xaa, 0xbc}) 98 | a.So(err, ShouldBeNil) 99 | 100 | select { 101 | case msg := <-uplink: 102 | a.So(msg.AppID, ShouldEqual, appID) 103 | a.So(msg.DevID, ShouldEqual, "sdk-test") 104 | case <-time.After(time.Second): 105 | t.Fatal("Did not receive uplink within a second") 106 | } 107 | 108 | err = dev.Delete() 109 | a.So(err, ShouldBeNil) 110 | } 111 | 112 | func TestCleanMQTTAddress(t *testing.T) { 113 | a := New(t) 114 | 115 | addr, err := cleanMQTTAddress("localhost:1883") 116 | a.So(err, ShouldBeNil) 117 | a.So(addr, ShouldEqual, "tcp://localhost:1883") 118 | 119 | // if `host:port` then `mqtt://host:port` 120 | addr, err = cleanMQTTAddress("localhost:1234") 121 | a.So(err, ShouldBeNil) 122 | a.So(addr, ShouldEqual, "tcp://localhost:1234") 123 | 124 | // if `host:8883` then `mqtts://host:8883` 125 | addr, err = cleanMQTTAddress("localhost:8883") 126 | a.So(err, ShouldBeNil) 127 | a.So(addr, ShouldEqual, "ssl://localhost:8883") 128 | 129 | // if `host` then `mqtt://host:1883` and `mqtts://host:8883` (we choose mqtts) 130 | addr, err = cleanMQTTAddress("localhost") 131 | a.So(err, ShouldBeNil) 132 | a.So(addr, ShouldEqual, "ssl://localhost:8883") 133 | 134 | // if `mqtt://host` then `mqtt://host:1883` 135 | addr, err = cleanMQTTAddress("mqtt://localhost") 136 | a.So(err, ShouldBeNil) 137 | a.So(addr, ShouldEqual, "tcp://localhost:1883") 138 | 139 | // if `mqtts://host` then `mqtt://host:1883` and `mqtts://host:8883` (we choose mqtts) 140 | addr, err = cleanMQTTAddress("mqtts://localhost") 141 | a.So(err, ShouldBeNil) 142 | a.So(addr, ShouldEqual, "ssl://localhost:8883") 143 | 144 | } 145 | -------------------------------------------------------------------------------- /mocks_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "github.com/TheThingsNetwork/api/handler" 8 | "github.com/TheThingsNetwork/api/protocol/lorawan" 9 | ptypes "github.com/gogo/protobuf/types" 10 | "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | ) 13 | 14 | type mockApplicationManagerClient struct { 15 | application *handler.Application 16 | applicationIdentifier *handler.ApplicationIdentifier 17 | ctx context.Context 18 | device *handler.Device 19 | deviceIdentifier *handler.DeviceIdentifier 20 | deviceList *handler.DeviceList 21 | dryDownlinkMessage *handler.DryDownlinkMessage 22 | dryDownlinkResult *handler.DryDownlinkResult 23 | dryUplinkMessage *handler.DryUplinkMessage 24 | dryUplinkResult *handler.DryUplinkResult 25 | empty *ptypes.Empty 26 | err error 27 | SimulatedUplinkMessage *handler.SimulatedUplinkMessage 28 | } 29 | 30 | func (m *mockApplicationManagerClient) reset() { 31 | m.application = nil 32 | m.applicationIdentifier = nil 33 | m.ctx = nil 34 | m.device = nil 35 | m.deviceIdentifier = nil 36 | m.deviceList = nil 37 | m.dryDownlinkMessage = nil 38 | m.dryDownlinkResult = nil 39 | m.dryUplinkMessage = nil 40 | m.dryUplinkResult = nil 41 | m.empty = nil 42 | m.err = nil 43 | m.SimulatedUplinkMessage = nil 44 | } 45 | 46 | func (m *mockApplicationManagerClient) RegisterApplication(ctx context.Context, in *handler.ApplicationIdentifier, opts ...grpc.CallOption) (*ptypes.Empty, error) { 47 | m.ctx = ctx 48 | m.applicationIdentifier = in 49 | return m.empty, m.err 50 | } 51 | func (m *mockApplicationManagerClient) GetApplication(ctx context.Context, in *handler.ApplicationIdentifier, opts ...grpc.CallOption) (*handler.Application, error) { 52 | m.ctx = ctx 53 | m.applicationIdentifier = in 54 | return m.application, m.err 55 | } 56 | func (m *mockApplicationManagerClient) SetApplication(ctx context.Context, in *handler.Application, opts ...grpc.CallOption) (*ptypes.Empty, error) { 57 | m.ctx = ctx 58 | m.application = in 59 | return m.empty, m.err 60 | } 61 | func (m *mockApplicationManagerClient) DeleteApplication(ctx context.Context, in *handler.ApplicationIdentifier, opts ...grpc.CallOption) (*ptypes.Empty, error) { 62 | m.ctx = ctx 63 | m.applicationIdentifier = in 64 | return m.empty, m.err 65 | } 66 | func (m *mockApplicationManagerClient) GetDevice(ctx context.Context, in *handler.DeviceIdentifier, opts ...grpc.CallOption) (*handler.Device, error) { 67 | m.ctx = ctx 68 | m.deviceIdentifier = in 69 | return m.device, m.err 70 | } 71 | func (m *mockApplicationManagerClient) SetDevice(ctx context.Context, in *handler.Device, opts ...grpc.CallOption) (*ptypes.Empty, error) { 72 | m.ctx = ctx 73 | m.device = in 74 | return m.empty, m.err 75 | } 76 | func (m *mockApplicationManagerClient) DeleteDevice(ctx context.Context, in *handler.DeviceIdentifier, opts ...grpc.CallOption) (*ptypes.Empty, error) { 77 | m.ctx = ctx 78 | m.deviceIdentifier = in 79 | return m.empty, m.err 80 | } 81 | func (m *mockApplicationManagerClient) GetDevicesForApplication(ctx context.Context, in *handler.ApplicationIdentifier, opts ...grpc.CallOption) (*handler.DeviceList, error) { 82 | m.ctx = ctx 83 | m.applicationIdentifier = in 84 | return m.deviceList, m.err 85 | } 86 | func (m *mockApplicationManagerClient) DryDownlink(ctx context.Context, in *handler.DryDownlinkMessage, opts ...grpc.CallOption) (*handler.DryDownlinkResult, error) { 87 | m.ctx = ctx 88 | m.dryDownlinkMessage = in 89 | return m.dryDownlinkResult, m.err 90 | } 91 | func (m *mockApplicationManagerClient) DryUplink(ctx context.Context, in *handler.DryUplinkMessage, opts ...grpc.CallOption) (*handler.DryUplinkResult, error) { 92 | m.ctx = ctx 93 | m.dryUplinkMessage = in 94 | return m.dryUplinkResult, m.err 95 | } 96 | func (m *mockApplicationManagerClient) SimulateUplink(ctx context.Context, in *handler.SimulatedUplinkMessage, opts ...grpc.CallOption) (*ptypes.Empty, error) { 97 | m.ctx = ctx 98 | m.SimulatedUplinkMessage = in 99 | return m.empty, m.err 100 | } 101 | 102 | type mockDevAddrManagerClient struct { 103 | ctx context.Context 104 | devAddrRequest *lorawan.DevAddrRequest 105 | devAddrResponse *lorawan.DevAddrResponse 106 | err error 107 | prefixesRequest *lorawan.PrefixesRequest 108 | prefixesResponse *lorawan.PrefixesResponse 109 | } 110 | 111 | func (m *mockDevAddrManagerClient) reset() { 112 | m.ctx = nil 113 | m.devAddrRequest = nil 114 | m.devAddrResponse = nil 115 | m.err = nil 116 | m.prefixesRequest = nil 117 | m.prefixesResponse = nil 118 | } 119 | 120 | func (m *mockDevAddrManagerClient) GetPrefixes(ctx context.Context, in *lorawan.PrefixesRequest, opts ...grpc.CallOption) (*lorawan.PrefixesResponse, error) { 121 | m.ctx = ctx 122 | m.prefixesRequest = in 123 | return m.prefixesResponse, m.err 124 | } 125 | func (m *mockDevAddrManagerClient) GetDevAddr(ctx context.Context, in *lorawan.DevAddrRequest, opts ...grpc.CallOption) (*lorawan.DevAddrResponse, error) { 126 | m.ctx = ctx 127 | m.devAddrRequest = in 128 | return m.devAddrResponse, m.err 129 | } 130 | -------------------------------------------------------------------------------- /mqtt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 13 | testlog "github.com/TheThingsNetwork/go-utils/log/test" 14 | "github.com/TheThingsNetwork/ttn/core/types" 15 | "github.com/TheThingsNetwork/ttn/mqtt" 16 | . "github.com/smartystreets/assertions" 17 | ) 18 | 19 | var mqttAddress string 20 | 21 | func init() { 22 | mqttAddress = os.Getenv("MQTT_ADDRESS") 23 | if mqttAddress == "" { 24 | mqttAddress = "localhost:1883" 25 | } 26 | } 27 | 28 | func TestMQTT(t *testing.T) { 29 | a := New(t) 30 | 31 | log := testlog.NewLogger() 32 | ttnlog.Set(log) 33 | defer log.Print(t) 34 | 35 | c := new(client) 36 | c.Logger = log 37 | defer c.Close() 38 | 39 | c.appID = "test" 40 | c.mqtt.ctx, c.mqtt.cancel = context.WithCancel(context.Background()) 41 | c.mqtt.client = mqtt.NewClient(log, "test-mqtt", "", "", "tcp://"+mqttAddress) 42 | err := c.mqtt.client.Connect() 43 | a.So(err, ShouldBeNil) 44 | 45 | pubsub, err := c.PubSub() 46 | a.So(err, ShouldBeNil) 47 | 48 | defer pubsub.Close() 49 | 50 | testDevice := pubsub.Device("test") 51 | defer testDevice.Close() 52 | allDevices := pubsub.AllDevices() 53 | defer allDevices.Close() 54 | 55 | testUplink, err := testDevice.SubscribeUplink() 56 | a.So(err, ShouldBeNil) 57 | 58 | allUplink, err := allDevices.SubscribeUplink() 59 | a.So(err, ShouldBeNil) 60 | 61 | c.mqtt.client.PublishUplink(types.UplinkMessage{ 62 | AppID: "test", 63 | DevID: "other", 64 | PayloadRaw: []byte{0x01, 0x02, 0x03, 0x04}, 65 | }).Wait() 66 | 67 | c.mqtt.client.PublishUplink(types.UplinkMessage{ 68 | AppID: "test", 69 | DevID: "test", 70 | PayloadRaw: []byte{0x01, 0x02, 0x03, 0x04}, 71 | }).Wait() 72 | 73 | select { 74 | case msg := <-testUplink: 75 | a.So(msg.DevID, ShouldEqual, "test") 76 | a.So(msg.PayloadRaw, ShouldResemble, []byte{0x01, 0x02, 0x03, 0x04}) 77 | case <-time.After(time.Second): 78 | t.Fatal("Did not receive from testUplink within a second") 79 | } 80 | 81 | for i := 0; i < 2; i++ { 82 | select { 83 | case msg := <-allUplink: 84 | a.So(msg.AppID, ShouldEqual, "test") 85 | a.So(msg.PayloadRaw, ShouldResemble, []byte{0x01, 0x02, 0x03, 0x04}) 86 | case <-time.After(time.Second): 87 | t.Fatalf("Did not receive %d from allUplink within a second", i) 88 | } 89 | } 90 | 91 | a.So(testDevice.UnsubscribeUplink(), ShouldBeNil) 92 | a.So(allDevices.UnsubscribeUplink(), ShouldBeNil) 93 | 94 | testEvent, err := testDevice.SubscribeEvents() 95 | a.So(err, ShouldBeNil) 96 | 97 | allEvent, err := allDevices.SubscribeEvents() 98 | a.So(err, ShouldBeNil) 99 | 100 | c.mqtt.client.PublishDeviceEvent("test", "other", types.UplinkErrorEvent, types.ErrorEventData{ 101 | Error: "some error", 102 | }).Wait() 103 | 104 | c.mqtt.client.PublishDeviceEvent("test", "test", types.UplinkErrorEvent, types.ErrorEventData{ 105 | Error: "some error", 106 | }).Wait() 107 | 108 | select { 109 | case msg := <-testEvent: 110 | a.So(msg.DevID, ShouldEqual, "test") 111 | a.So(msg.Data, ShouldNotBeNil) 112 | a.So(msg.Data, ShouldHaveSameTypeAs, new(types.ErrorEventData)) 113 | case <-time.After(time.Second): 114 | t.Fatal("Did not receive from testEvent within a second") 115 | } 116 | 117 | for i := 0; i < 2; i++ { 118 | select { 119 | case msg := <-allEvent: 120 | a.So(msg.AppID, ShouldEqual, "test") 121 | a.So(msg.Data, ShouldNotBeNil) 122 | a.So(msg.Data, ShouldHaveSameTypeAs, new(types.ErrorEventData)) 123 | case <-time.After(time.Second): 124 | t.Fatalf("Did not receive %d from allEvent within a second", i) 125 | } 126 | } 127 | 128 | a.So(testDevice.UnsubscribeEvents(), ShouldBeNil) 129 | a.So(allDevices.UnsubscribeEvents(), ShouldBeNil) 130 | 131 | testActivation, err := testDevice.SubscribeActivations() 132 | a.So(err, ShouldBeNil) 133 | 134 | allActivation, err := allDevices.SubscribeActivations() 135 | a.So(err, ShouldBeNil) 136 | 137 | c.mqtt.client.PublishActivation(types.Activation{ 138 | AppID: "test", 139 | DevID: "other", 140 | AppEUI: types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}, 141 | }).Wait() 142 | 143 | c.mqtt.client.PublishActivation(types.Activation{ 144 | AppID: "test", 145 | DevID: "test", 146 | AppEUI: types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}, 147 | }).Wait() 148 | 149 | select { 150 | case msg := <-testActivation: 151 | a.So(msg.DevID, ShouldEqual, "test") 152 | case <-time.After(time.Second): 153 | t.Fatal("Did not receive from testActivation within a second") 154 | } 155 | 156 | for i := 0; i < 2; i++ { 157 | select { 158 | case msg := <-allActivation: 159 | a.So(msg.AppID, ShouldEqual, "test") 160 | 161 | case <-time.After(time.Second): 162 | t.Fatalf("Did not receive %d from allActivation within a second", i) 163 | } 164 | } 165 | 166 | a.So(testDevice.UnsubscribeActivations(), ShouldBeNil) 167 | a.So(allDevices.UnsubscribeActivations(), ShouldBeNil) 168 | 169 | downlink := make(chan *types.DownlinkMessage) 170 | c.mqtt.client.SubscribeDownlink(func(_ mqtt.Client, appID string, devID string, msg types.DownlinkMessage) { 171 | downlink <- &msg 172 | }) 173 | 174 | err = pubsub.Publish("test", &types.DownlinkMessage{ 175 | AppID: "test", 176 | DevID: "test", 177 | PayloadRaw: []byte{0x01, 0x02, 0x03, 0x04}, 178 | }) 179 | a.So(err, ShouldBeNil) 180 | 181 | select { 182 | case msg := <-downlink: 183 | a.So(msg.AppID, ShouldEqual, "test") 184 | a.So(msg.DevID, ShouldEqual, "test") 185 | a.So(msg.PayloadRaw, ShouldResemble, []byte{0x01, 0x02, 0x03, 0x04}) 186 | case <-time.After(time.Second): 187 | t.Fatal("Did not receive on downlink within a second") 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /application_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/api/handler" 13 | "github.com/TheThingsNetwork/go-utils/log" 14 | ) 15 | 16 | // ApplicationManager manages an application. 17 | type ApplicationManager interface { 18 | // Get the payload format used in this application. If the payload format is "custom", you can get the custom JS 19 | // payload functions with the GetCustomPayloadFunctions() function. 20 | GetPayloadFormat() (string, error) 21 | 22 | // Set the payload format to use in this application. If you want to use custom JS payload functions, use the 23 | // SetCustomPayloadFunctions() function instead. If you want to disable payload conversion, pass an empty string. 24 | SetPayloadFormat(format string) error 25 | 26 | // Get the custom JS payload functions. 27 | GetCustomPayloadFunctions() (jsDecoder, jsConverter, jsValidator, jsEncoder string, err error) 28 | 29 | // Set the custom JS payload functions. 30 | // 31 | // Example Decoder: 32 | // 33 | // // Decoder (Array, uint8) returns (Object) 34 | // function Decoder(bytes, port) { 35 | // var decoded = {}; 36 | // return decoded; 37 | // } 38 | // 39 | // Example Converter: 40 | // 41 | // // Converter (Object, uint8) returns (Object) 42 | // function Converter(decoded, port) { 43 | // var converted = decoded; 44 | // return converted; 45 | // } 46 | // 47 | // Example Validator: 48 | // // Validator (Object, uint8) returns (Boolean) 49 | // function Validator(converted, port) { 50 | // return true; 51 | // } 52 | // 53 | // Example Encoder: 54 | // 55 | // // Validator (Object, uint8) returns (Array) 56 | // function Encoder(object, port) { 57 | // var bytes = []; 58 | // return bytes; 59 | // } 60 | SetCustomPayloadFunctions(jsDecoder, jsConverter, jsValidator, jsEncoder string) error 61 | } 62 | 63 | func (c *client) ManageApplication() (ApplicationManager, error) { 64 | if err := c.connectHandler(); err != nil { 65 | return nil, err 66 | } 67 | return &applicationManager{ 68 | logger: c.Logger, 69 | client: handler.NewApplicationManagerClient(c.handler.conn), 70 | getContext: c.getContext, 71 | requestTimeout: c.RequestTimeout, 72 | appID: c.appID, 73 | }, nil 74 | } 75 | 76 | type applicationManager struct { 77 | logger log.Interface 78 | client handler.ApplicationManagerClient 79 | getContext func(context.Context) context.Context 80 | requestTimeout time.Duration 81 | 82 | appID string 83 | } 84 | 85 | func (a *applicationManager) getApplication() (*handler.Application, error) { 86 | ctx, cancel := context.WithTimeout(a.getContext(context.Background()), a.requestTimeout) 87 | defer cancel() 88 | return a.client.GetApplication(ctx, &handler.ApplicationIdentifier{AppID: a.appID}) 89 | } 90 | 91 | func (a *applicationManager) setApplication(app *handler.Application) error { 92 | ctx, cancel := context.WithTimeout(a.getContext(context.Background()), a.requestTimeout) 93 | defer cancel() 94 | _, err := a.client.SetApplication(ctx, app) 95 | return err 96 | } 97 | 98 | func (a *applicationManager) GetPayloadFormat() (string, error) { 99 | app, err := a.getApplication() 100 | if err != nil { 101 | return "", err 102 | } 103 | return app.PayloadFormat, nil 104 | } 105 | 106 | func (a *applicationManager) SetPayloadFormat(format string) error { 107 | app, err := a.getApplication() 108 | if err != nil { 109 | return err 110 | } 111 | app.PayloadFormat = format 112 | if app.PayloadFormat != "custom" { 113 | app.Decoder, app.Converter, app.Validator, app.Encoder = "", "", "", "" 114 | } 115 | return a.setApplication(app) 116 | } 117 | 118 | func (a *applicationManager) GetCustomPayloadFunctions() (jsDecoder, jsConverter, jsValidator, jsEncoder string, err error) { 119 | app, err := a.getApplication() 120 | if err != nil { 121 | return jsDecoder, jsConverter, jsValidator, jsEncoder, err 122 | } 123 | if app.PayloadFormat != "custom" { 124 | return jsDecoder, jsConverter, jsValidator, jsEncoder, fmt.Errorf("ttn-sdk: application does not have custom payload functions, but uses \"%s\"", app.PayloadFormat) 125 | } 126 | return app.Decoder, app.Converter, app.Validator, app.Encoder, nil 127 | } 128 | 129 | func (a *applicationManager) SetCustomPayloadFunctions(jsDecoder, jsConverter, jsValidator, jsEncoder string) error { 130 | app, err := a.getApplication() 131 | if err != nil { 132 | return err 133 | } 134 | app.PayloadFormat = "custom" 135 | app.Decoder, app.Converter, app.Validator, app.Encoder = jsDecoder, jsConverter, jsValidator, jsEncoder 136 | return a.setApplication(app) 137 | } 138 | 139 | func (a *applicationManager) TestCustomUplinkPayloadFunctions(jsDecoder, jsConverter, jsValidator string, payload []byte, port uint8) (*handler.DryUplinkResult, error) { 140 | ctx, cancel := context.WithTimeout(a.getContext(context.Background()), a.requestTimeout) 141 | defer cancel() 142 | return a.client.DryUplink(ctx, &handler.DryUplinkMessage{ 143 | Payload: payload, 144 | App: handler.Application{ 145 | AppID: a.appID, 146 | PayloadFormat: "custom", 147 | Decoder: jsDecoder, 148 | Converter: jsConverter, 149 | Validator: jsValidator, 150 | }, 151 | Port: uint32(port), 152 | }) 153 | } 154 | 155 | func (a *applicationManager) TestCustomDownlinkPayloadFunctions(jsEncoder string, fields map[string]interface{}, port uint8) (*handler.DryDownlinkResult, error) { 156 | ctx, cancel := context.WithTimeout(a.getContext(context.Background()), a.requestTimeout) 157 | defer cancel() 158 | fieldsJSON, err := json.Marshal(fields) 159 | if err != nil { 160 | return nil, err 161 | } 162 | return a.client.DryDownlink(ctx, &handler.DryDownlinkMessage{ 163 | Fields: string(fieldsJSON), 164 | App: handler.Application{ 165 | AppID: a.appID, 166 | PayloadFormat: "custom", 167 | Encoder: jsEncoder, 168 | }, 169 | Port: uint32(port), 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "crypto/tls" 9 | "sync" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/api/discovery" 13 | "github.com/TheThingsNetwork/go-account-lib/account" 14 | "github.com/TheThingsNetwork/go-utils/grpc/restartstream" 15 | "github.com/TheThingsNetwork/go-utils/grpc/rpclog" 16 | "github.com/TheThingsNetwork/go-utils/grpc/ttnctx" 17 | "github.com/TheThingsNetwork/go-utils/log" 18 | "github.com/TheThingsNetwork/ttn/mqtt" 19 | "github.com/mwitkow/go-grpc-middleware" 20 | "google.golang.org/grpc" 21 | "google.golang.org/grpc/credentials" 22 | ) 23 | 24 | // ClientVersion to use 25 | var ClientVersion = "2.x.x" 26 | 27 | // ClientConfig contains the configuration for the API client. Use the NewConfig() or NewCommunityConfig() functions to 28 | // initialize your configuration, otherwise NewClient will panic. 29 | type ClientConfig struct { 30 | initialized bool 31 | 32 | Logger log.Interface 33 | 34 | // The name of this client 35 | ClientName string 36 | 37 | // The version of this client (in the default config, this is the value of ttnsdk.ClientVersion) 38 | ClientVersion string 39 | 40 | // TLS Configuration only has to be set if connecting with servers that do not have trusted certificates. 41 | TLSConfig *tls.Config 42 | 43 | // Address of the Account Server (in the default config, this is https://account.thethingsnetwork.org) 44 | AccountServerAddress string 45 | 46 | // Client ID for the account server (if you registered your client) 47 | AccountServerClientID string 48 | 49 | // Client Secret for the account server (if you registered your client) 50 | AccountServerClientSecret string 51 | 52 | // Address of the Discovery Server (in the default config, this is discovery.thethings.network:1900) 53 | DiscoveryServerAddress string 54 | 55 | // Set this to true if the Discovery Server is insecure (not recommended) 56 | DiscoveryServerInsecure bool 57 | 58 | // Address of the Handler (optional) 59 | HandlerAddress string 60 | 61 | // Timeout for requests (in the default config, this is 10 seconds) 62 | RequestTimeout time.Duration 63 | 64 | appID string 65 | appAccessKey string 66 | } 67 | 68 | // NewCommunityConfig creates a new configuration for the API client that is pre-configured for the Public Community Network. 69 | func NewCommunityConfig(clientName string) ClientConfig { 70 | return NewConfig(clientName, "https://account.thethingsnetwork.org", "discovery.thethings.network:1900") 71 | } 72 | 73 | // NewConfig creates a new configuration for the API client. 74 | func NewConfig(clientName, accountServerAddress, discoveryServerAddress string) ClientConfig { 75 | return ClientConfig{ 76 | initialized: true, 77 | Logger: log.Get(), 78 | ClientName: clientName, 79 | ClientVersion: ClientVersion, 80 | AccountServerAddress: accountServerAddress, 81 | AccountServerClientID: clientName, 82 | DiscoveryServerAddress: discoveryServerAddress, 83 | RequestTimeout: 10 * time.Second, 84 | } 85 | } 86 | 87 | // NewClient creates a new API client from the configuration, using the given Application ID and Application access key. 88 | func (c ClientConfig) NewClient(appID, appAccessKey string) Client { 89 | c.appID = appID 90 | c.appAccessKey = appAccessKey 91 | return newClient(c) 92 | } 93 | 94 | // DialOptions to use when connecting to components 95 | var DialOptions = []grpc.DialOption{ 96 | grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient( 97 | rpclog.UnaryClientInterceptor(nil), 98 | )), 99 | grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient( 100 | restartstream.Interceptor(restartstream.DefaultSettings), 101 | rpclog.StreamClientInterceptor(nil), 102 | )), 103 | grpc.WithBlock(), 104 | } 105 | 106 | func newClient(config ClientConfig) Client { 107 | if !config.initialized { 108 | panic("ttn-sdk: ClientConfig not initialized. Use ttnsdk.NewConfig or ttnsdk.NewCommunityConfig to generate your configuration") 109 | } 110 | client := &client{ 111 | ClientConfig: config, 112 | transportCredentials: credentials.NewTLS(config.TLSConfig), 113 | } 114 | if config.AccountServerAddress != "" { 115 | client.account = account.New(config.AccountServerAddress) 116 | } 117 | return client 118 | } 119 | 120 | // Client interface for The Things Network's API. 121 | type Client interface { 122 | // Close the client and clean up all connections 123 | Close() error 124 | 125 | // Subscribe to uplink and events, publish downlink 126 | PubSub() (ApplicationPubSub, error) 127 | 128 | // Manage the application 129 | ManageApplication() (ApplicationManager, error) 130 | 131 | // Manage devices in the application 132 | ManageDevices() (DeviceManager, error) 133 | 134 | // Simulate uplink messages for a device (for testing) 135 | Simulate(devID string) (Simulator, error) 136 | } 137 | 138 | type client struct { 139 | ClientConfig 140 | transportCredentials credentials.TransportCredentials 141 | account *account.Account 142 | discovery struct { 143 | sync.RWMutex 144 | conn *grpc.ClientConn 145 | } 146 | handler struct { 147 | sync.RWMutex 148 | announcement *discovery.Announcement 149 | conn *grpc.ClientConn 150 | } 151 | mqtt struct { 152 | sync.RWMutex 153 | client mqtt.Client 154 | ctx context.Context 155 | cancel context.CancelFunc 156 | } 157 | } 158 | 159 | func (c *client) getContext(ctx context.Context) context.Context { 160 | ctx = ttnctx.OutgoingContextWithServiceInfo(ctx, c.ClientConfig.ClientName, c.ClientConfig.ClientVersion, "") 161 | if c.appAccessKey != "" { 162 | ctx = ttnctx.OutgoingContextWithKey(ctx, c.appAccessKey) 163 | } 164 | return ctx 165 | } 166 | 167 | func (c *client) Close() (closeErr error) { 168 | if err := c.closeHandler(); err != nil { 169 | closeErr = err 170 | } 171 | if err := c.closeDiscovery(); err != nil && closeErr == nil { 172 | closeErr = err 173 | } 174 | if err := c.closeMQTT(); err != nil && closeErr == nil { 175 | closeErr = err 176 | } 177 | return 178 | } 179 | -------------------------------------------------------------------------------- /device_manager_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | "time" 11 | 12 | "github.com/TheThingsNetwork/api/handler" 13 | "github.com/TheThingsNetwork/api/protocol/lorawan" 14 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 15 | testlog "github.com/TheThingsNetwork/go-utils/log/test" 16 | "github.com/TheThingsNetwork/ttn/core/types" 17 | . "github.com/smartystreets/assertions" 18 | ) 19 | 20 | func TestDeviceManager(t *testing.T) { 21 | a := New(t) 22 | 23 | log := testlog.NewLogger() 24 | ttnlog.Set(log) 25 | defer log.Print(t) 26 | 27 | mock := new(mockApplicationManagerClient) 28 | devMock := new(mockDevAddrManagerClient) 29 | 30 | manager := &deviceManager{ 31 | logger: log, 32 | client: mock, 33 | devAddrClient: devMock, 34 | getContext: func(ctx context.Context) context.Context { return ctx }, 35 | requestTimeout: time.Second, 36 | appID: "test", 37 | } 38 | 39 | someErr := errors.New("some error") 40 | 41 | { 42 | mock.reset() 43 | mock.err = someErr 44 | _, err := manager.List(0, 0) 45 | a.So(err, ShouldNotBeNil) 46 | 47 | mock.reset() 48 | mock.deviceList = &handler.DeviceList{Devices: []*handler.Device{ 49 | &handler.Device{ 50 | DevID: "dev-id", 51 | Device: &handler.Device_LoRaWANDevice{LoRaWANDevice: &lorawan.Device{ 52 | AppEUI: types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}, 53 | DevEUI: types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}, 54 | }}, 55 | }, 56 | }} 57 | sparseDevices, err := manager.List(0, 0) 58 | a.So(err, ShouldBeNil) 59 | a.So(mock.applicationIdentifier, ShouldNotBeNil) 60 | a.So(mock.applicationIdentifier.AppID, ShouldEqual, "test") 61 | devices := sparseDevices.AsDevices() 62 | a.So(devices, ShouldHaveLength, 1) 63 | a.So(devices[0].DevID, ShouldEqual, "dev-id") 64 | a.So(devices[0].AppEUI, ShouldEqual, types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}) 65 | a.So(devices[0].DevEUI, ShouldEqual, types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}) 66 | } 67 | 68 | { 69 | mock.reset() 70 | mock.err = someErr 71 | _, err := manager.Get("dev-id") 72 | a.So(err, ShouldNotBeNil) 73 | 74 | mock.reset() 75 | mock.device = &handler.Device{ 76 | DevID: "dev-id", 77 | Device: &handler.Device_LoRaWANDevice{LoRaWANDevice: &lorawan.Device{ 78 | AppEUI: types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}, 79 | DevEUI: types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}, 80 | FCntDown: 42, 81 | }}, 82 | } 83 | device, err := manager.Get("dev-id") 84 | a.So(err, ShouldBeNil) 85 | a.So(mock.deviceIdentifier, ShouldNotBeNil) 86 | a.So(mock.deviceIdentifier.AppID, ShouldEqual, "test") 87 | a.So(mock.deviceIdentifier.DevID, ShouldEqual, "dev-id") 88 | a.So(device.DevID, ShouldEqual, "dev-id") 89 | a.So(device.AppEUI, ShouldEqual, types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}) 90 | a.So(device.DevEUI, ShouldEqual, types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}) 91 | a.So(device.FCntDown, ShouldEqual, 42) 92 | } 93 | 94 | { 95 | mock.reset() 96 | mock.err = someErr 97 | err := manager.Set(&Device{}) 98 | a.So(err, ShouldNotBeNil) 99 | 100 | mock.reset() 101 | err = manager.Set(&Device{ 102 | SparseDevice: SparseDevice{ 103 | DevID: "dev-id", 104 | AppEUI: types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}, 105 | DevEUI: types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}, 106 | }, 107 | FCntDown: 42, 108 | }) 109 | a.So(err, ShouldBeNil) 110 | a.So(mock.device, ShouldNotBeNil) 111 | a.So(mock.device.DevID, ShouldEqual, "dev-id") 112 | a.So(mock.device.GetLoRaWANDevice().DevID, ShouldEqual, "dev-id") 113 | a.So(mock.device.GetLoRaWANDevice().AppEUI, ShouldResemble, types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}) 114 | a.So(mock.device.GetLoRaWANDevice().DevEUI, ShouldResemble, types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}) 115 | a.So(mock.device.GetLoRaWANDevice().FCntDown, ShouldEqual, 42) 116 | } 117 | 118 | { 119 | mock.reset() 120 | mock.err = someErr 121 | err := manager.Delete("dev-id") 122 | a.So(err, ShouldNotBeNil) 123 | 124 | mock.reset() 125 | err = manager.Delete("dev-id") 126 | a.So(err, ShouldBeNil) 127 | a.So(mock.deviceIdentifier, ShouldNotBeNil) 128 | a.So(mock.deviceIdentifier.AppID, ShouldEqual, "test") 129 | a.So(mock.deviceIdentifier.DevID, ShouldEqual, "dev-id") 130 | } 131 | 132 | { 133 | dev := new(Device) 134 | a.So(dev.IsNew(), ShouldBeTrue) 135 | // Can't call these funcs on a new device 136 | a.So(func() { dev.Update() }, ShouldPanic) 137 | a.So(func() { dev.Personalize(types.NwkSKey{}, types.AppSKey{}) }, ShouldPanic) 138 | a.So(func() { dev.Delete() }, ShouldPanic) 139 | } 140 | 141 | { 142 | dev := new(Device) 143 | a.So(dev.IsNew(), ShouldBeTrue) 144 | dev.SetManager(manager) 145 | a.So(dev.IsNew(), ShouldBeFalse) 146 | // You can set the same manager 147 | a.So(func() { dev.SetManager(manager) }, ShouldNotPanic) 148 | // But you can't change the manager 149 | otherManager := &deviceManager{} 150 | a.So(func() { dev.SetManager(otherManager) }, ShouldPanic) 151 | } 152 | 153 | { 154 | mock.reset() 155 | mock.device = &handler.Device{ 156 | DevID: "dev-id", 157 | Device: &handler.Device_LoRaWANDevice{LoRaWANDevice: &lorawan.Device{ 158 | AppEUI: types.AppEUI{1, 2, 3, 4, 5, 6, 7, 8}, 159 | DevEUI: types.DevEUI{1, 2, 3, 4, 5, 6, 7, 8}, 160 | FCntDown: 42, 161 | }}, 162 | } 163 | device, err := manager.Get("dev-id") 164 | a.So(err, ShouldBeNil) 165 | 166 | device.FCntDown = 0 167 | err = device.Update() 168 | a.So(err, ShouldBeNil) 169 | a.So(mock.device.GetLoRaWANDevice().FCntDown, ShouldEqual, 0) 170 | 171 | mock.reset() 172 | devMock.reset() 173 | devMock.err = someErr 174 | err = device.Personalize(types.NwkSKey{}, types.AppSKey{}) 175 | a.So(err, ShouldNotBeNil) 176 | 177 | mock.reset() 178 | devMock.reset() 179 | devMock.devAddrResponse = &lorawan.DevAddrResponse{DevAddr: types.DevAddr{1, 2, 3, 4}} 180 | err = device.Personalize(types.NwkSKey{}, types.AppSKey{}) 181 | a.So(err, ShouldBeNil) 182 | a.So(mock.device.GetLoRaWANDevice().DevAddr, ShouldResemble, &types.DevAddr{1, 2, 3, 4}) 183 | 184 | mock.reset() 185 | err = device.Delete() 186 | a.So(err, ShouldBeNil) 187 | a.So(mock.deviceIdentifier.DevID, ShouldEqual, "dev-id") 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /sdk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk_test 5 | 6 | import ( 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/hex" 10 | "encoding/json" 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | 15 | ttnsdk "github.com/TheThingsNetwork/go-app-sdk" 16 | ttnlog "github.com/TheThingsNetwork/go-utils/log" 17 | "github.com/TheThingsNetwork/go-utils/log/apex" 18 | "github.com/TheThingsNetwork/go-utils/random" 19 | "github.com/TheThingsNetwork/ttn/core/types" 20 | ) 21 | 22 | const ( 23 | sdkClientName = "my-amazing-app" 24 | ) 25 | 26 | func Example() { 27 | log := apex.Stdout() // We use a cli logger at Stdout 28 | log.MustParseLevel("debug") 29 | ttnlog.Set(log) // Set the logger as default for TTN 30 | 31 | // We get the application ID and application access key from the environment 32 | appID := os.Getenv("TTN_APP_ID") 33 | appAccessKey := os.Getenv("TTN_APP_ACCESS_KEY") 34 | 35 | // Create a new SDK configuration for the public community network 36 | config := ttnsdk.NewCommunityConfig(sdkClientName) 37 | config.ClientVersion = "2.0.5" // The version of the application 38 | 39 | // If you connect to a private network that does not use trusted certificates on the Discovery Server 40 | // (from Let's Encrypt for example), you have to manually trust the certificates. If you use the public community 41 | // network, you can just delete the next code block. 42 | if caCert := os.Getenv("TTN_CA_CERT"); caCert != "" { 43 | config.TLSConfig = new(tls.Config) 44 | certBytes, err := ioutil.ReadFile(caCert) 45 | if err != nil { 46 | log.WithError(err).Fatal("my-amazing-app: could not read CA certificate file") 47 | } 48 | config.TLSConfig.RootCAs = x509.NewCertPool() 49 | if ok := config.TLSConfig.RootCAs.AppendCertsFromPEM(certBytes); !ok { 50 | log.Fatal("my-amazing-app: could not read CA certificates") 51 | } 52 | } 53 | 54 | // Create a new SDK client for the application 55 | client := config.NewClient(appID, appAccessKey) 56 | 57 | // Make sure the client is closed before the function returns 58 | // In your application, you should call this before the application shuts down 59 | defer client.Close() 60 | 61 | // Manage devices for the application. 62 | devices, err := client.ManageDevices() 63 | if err != nil { 64 | log.WithError(err).Fatal("my-amazing-app: could not get device manager") 65 | } 66 | 67 | // List the first 10 devices 68 | deviceList, err := devices.List(10, 0) 69 | if err != nil { 70 | log.WithError(err).Fatal("my-amazing-app: could not get devices") 71 | } 72 | log.Info("my-amazing-app: found devices") 73 | for _, device := range deviceList { 74 | fmt.Printf("- %s", device.DevID) 75 | } 76 | 77 | // Create a new device 78 | dev := new(ttnsdk.Device) 79 | dev.AppID = appID 80 | dev.DevID = "my-new-device" 81 | dev.Description = "A new device in my amazing app" 82 | dev.AppEUI = types.AppEUI{0x70, 0xB3, 0xD5, 0x7E, 0xF0, 0x00, 0x00, 0x24} // Use the real AppEUI here 83 | dev.DevEUI = types.DevEUI{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} // Use the real DevEUI here 84 | 85 | // Set a random AppKey 86 | dev.AppKey = new(types.AppKey) 87 | random.FillBytes(dev.AppKey[:]) 88 | 89 | err = devices.Set(dev) 90 | if err != nil { 91 | log.WithError(err).Fatal("my-amazing-app: could not create device") 92 | } 93 | 94 | // Get the device 95 | dev, err = devices.Get("my-new-device") 96 | if err != nil { 97 | log.WithError(err).Fatal("my-amazing-app: could not get device") 98 | } 99 | 100 | // Personalize the device with random session keys 101 | err = dev.PersonalizeRandom() 102 | if err != nil { 103 | log.WithError(err).Fatal("my-amazing-app: could not personalize device") 104 | } 105 | log.WithFields(ttnlog.Fields{ 106 | "devAddr": dev.DevAddr, 107 | "nwkSKey": dev.NwkSKey, 108 | "appSKey": dev.AppSKey, 109 | }).Info("my-amazing-app: personalized device") 110 | 111 | // Start Publish/Subscribe client (MQTT) 112 | pubsub, err := client.PubSub() 113 | if err != nil { 114 | log.WithError(err).Fatal("my-amazing-app: could not get application pub/sub") 115 | } 116 | 117 | // Make sure the pubsub client is closed before the function returns 118 | // In your application, you should call this before the application shuts down 119 | defer pubsub.Close() 120 | 121 | // Get a publish/subscribe client for all devices 122 | allDevicesPubSub := pubsub.AllDevices() 123 | 124 | // Make sure the pubsub client is closed before the function returns 125 | // In your application, you will probably call this before the application shuts down 126 | // This also stops existing subscriptions, in case you forgot to unsubscribe 127 | defer allDevicesPubSub.Close() 128 | 129 | // Subscribe to activations 130 | activations, err := allDevicesPubSub.SubscribeActivations() 131 | if err != nil { 132 | log.WithError(err).Fatal("my-amazing-app: could not subscribe to activations") 133 | } 134 | log.Debug("After this point, the program won't show anything until we receive an activation.") 135 | for activation := range activations { 136 | log.WithFields(ttnlog.Fields{ 137 | "appEUI": activation.AppEUI.String(), 138 | "devEUI": activation.DevEUI.String(), 139 | "devAddr": activation.DevAddr.String(), 140 | }).Info("my-amazing-app: received activation") 141 | break // normally you wouldn't do this 142 | } 143 | 144 | // Unsubscribe from activations 145 | err = allDevicesPubSub.UnsubscribeActivations() 146 | if err != nil { 147 | log.WithError(err).Fatal("my-amazing-app: could not unsubscribe from activations") 148 | } 149 | 150 | // Subscribe to events 151 | events, err := allDevicesPubSub.SubscribeEvents() 152 | if err != nil { 153 | log.WithError(err).Fatal("my-amazing-app: could not subscribe to events") 154 | } 155 | log.Debug("After this point, the program won't show anything until we receive an application event.") 156 | for event := range events { 157 | log.WithFields(ttnlog.Fields{ 158 | "devID": event.DevID, 159 | "eventType": event.Event, 160 | }).Info("my-amazing-app: received event") 161 | if event.Data != nil { 162 | eventJSON, _ := json.Marshal(event.Data) 163 | fmt.Println("Event data:" + string(eventJSON)) 164 | } 165 | break // normally you wouldn't do this 166 | } 167 | 168 | // Unsubscribe from events 169 | err = allDevicesPubSub.UnsubscribeEvents() 170 | if err != nil { 171 | log.WithError(err).Fatal("my-amazing-app: could not unsubscribe from events") 172 | } 173 | 174 | // Get a publish/subscribe client scoped to my-test-device 175 | myNewDevicePubSub := pubsub.Device("my-new-device") 176 | 177 | // Make sure the pubsub client for this device is closed before the function returns 178 | // In your application, you will probably call this when you no longer need the device 179 | // This also stops existing subscriptions, in case you forgot to unsubscribe 180 | defer myNewDevicePubSub.Close() 181 | 182 | // Subscribe to uplink messages 183 | uplink, err := myNewDevicePubSub.SubscribeUplink() 184 | if err != nil { 185 | log.WithError(err).Fatal("my-amazing-app: could not subscribe to uplink messages") 186 | } 187 | log.Debug("After this point, the program won't show anything until we receive an uplink message from device my-new-device.") 188 | for message := range uplink { 189 | hexPayload := hex.EncodeToString(message.PayloadRaw) 190 | log.WithField("data", hexPayload).Info("my-amazing-app: received uplink") 191 | break // normally you wouldn't do this 192 | } 193 | 194 | // Unsubscribe from uplink 195 | err = myNewDevicePubSub.UnsubscribeUplink() 196 | if err != nil { 197 | log.WithError(err).Fatal("my-amazing-app: could not unsubscribe from uplink") 198 | } 199 | 200 | // Publish downlink message 201 | err = myNewDevicePubSub.Publish(&types.DownlinkMessage{ 202 | AppID: appID, // can be left out, the SDK will fill this 203 | DevID: "my-new-device", // can be left out, the SDK will fill this 204 | PayloadRaw: []byte{0xaa, 0xbc}, 205 | FPort: 10, 206 | Schedule: types.ScheduleLast, // allowed values: "replace" (default), "first", "last" 207 | Confirmed: false, // can be left out, default is false 208 | }) 209 | if err != nil { 210 | log.WithError(err).Fatal("my-amazing-app: could not schedule downlink message") 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /mqtt.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "errors" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/TheThingsNetwork/go-utils/log" 14 | "github.com/TheThingsNetwork/ttn/core/types" 15 | "github.com/TheThingsNetwork/ttn/mqtt" 16 | ) 17 | 18 | func (c *client) connectMQTT() (err error) { 19 | c.mqtt.Lock() 20 | defer c.mqtt.Unlock() 21 | if c.mqtt.client != nil { 22 | return nil 23 | } 24 | c.handler.RLock() 25 | defer c.handler.RUnlock() 26 | if c.handler.announcement == nil { 27 | if err := c.discover(); err != nil { 28 | return err 29 | } 30 | } 31 | if c.handler.announcement.MqttAddress == "" { 32 | c.Logger.WithField("HandlerID", c.handler.announcement.ID).Debug("ttn-sdk: Handler does not announce MQTT address") 33 | return errors.New("ttn-sdk: Handler does not announce MQTT address") 34 | } 35 | mqttAddress, err := cleanMQTTAddress(c.handler.announcement.MqttAddress) 36 | if err != nil { 37 | return err 38 | } 39 | if strings.HasPrefix(mqttAddress, "ssl://") { 40 | c.mqtt.client = mqtt.NewTLSClient(c.Logger, c.ClientName, c.appID, c.appAccessKey, c.TLSConfig, mqttAddress) 41 | } else { 42 | c.mqtt.client = mqtt.NewClient(c.Logger, c.ClientName, c.appID, c.appAccessKey, mqttAddress) 43 | } 44 | c.mqtt.ctx, c.mqtt.cancel = context.WithCancel(context.Background()) 45 | logger := c.Logger.WithField("Address", mqttAddress) 46 | logger.Debug("ttn-sdk: Connecting to MQTT...") 47 | if err := c.mqtt.client.Connect(); err != nil { 48 | logger.WithError(err).Debug("ttn-sdk: Could not connect to MQTT") 49 | return err 50 | } 51 | logger.Debug("ttn-sdk: Connected to MQTT") 52 | return nil 53 | } 54 | 55 | func (c *client) closeMQTT() error { 56 | c.mqtt.Lock() 57 | defer c.mqtt.Unlock() 58 | if c.mqtt.client == nil { 59 | return nil 60 | } 61 | c.Logger.Debug("ttn-sdk: Disconnecting from MQTT...") 62 | c.mqtt.cancel() 63 | c.mqtt.client.Disconnect() 64 | c.mqtt.client = nil 65 | return nil 66 | } 67 | 68 | var mqttBufferSize = 10 69 | 70 | // DevicePub interface for publishing downlink messages to the device 71 | type DevicePub interface { 72 | Publish(*types.DownlinkMessage) error 73 | } 74 | 75 | // DeviceSub interface for subscribing to uplink messages and events from the device 76 | type DeviceSub interface { 77 | SubscribeUplink() (<-chan *types.UplinkMessage, error) 78 | UnsubscribeUplink() error 79 | SubscribeEvents() (<-chan *types.DeviceEvent, error) 80 | UnsubscribeEvents() error 81 | SubscribeActivations() (<-chan *types.Activation, error) 82 | UnsubscribeActivations() error 83 | Close() 84 | } 85 | 86 | // DevicePubSub combines the DevicePub and DeviceSub interfaces 87 | type DevicePubSub interface { 88 | DevicePub 89 | DeviceSub 90 | } 91 | 92 | type devicePubSub struct { 93 | logger log.Interface 94 | client mqtt.Client 95 | ctx context.Context 96 | cancel context.CancelFunc 97 | 98 | appID string 99 | devID string 100 | 101 | sync.RWMutex 102 | uplink chan *types.UplinkMessage 103 | events chan *types.DeviceEvent 104 | activations chan *types.Activation 105 | } 106 | 107 | func (d *devicePubSub) Publish(downlink *types.DownlinkMessage) error { 108 | msg := *downlink 109 | msg.AppID = d.appID 110 | msg.DevID = d.devID 111 | token := d.client.PublishDownlink(msg) 112 | token.Wait() 113 | return token.Error() 114 | } 115 | 116 | func (d *devicePubSub) SubscribeUplink() (<-chan *types.UplinkMessage, error) { 117 | if err := d.ctx.Err(); err != nil { 118 | return nil, err 119 | } 120 | d.Lock() 121 | defer d.Unlock() 122 | if d.uplink != nil { 123 | return d.uplink, nil 124 | } 125 | d.uplink = make(chan *types.UplinkMessage, mqttBufferSize) 126 | token := d.client.SubscribeDeviceUplink(d.appID, d.devID, func(_ mqtt.Client, appID string, devID string, msg types.UplinkMessage) { 127 | msg.AppID = appID 128 | msg.DevID = devID 129 | d.RLock() 130 | defer d.RUnlock() 131 | if d.uplink == nil { 132 | return 133 | } 134 | select { 135 | case d.uplink <- &msg: 136 | default: 137 | } 138 | }) 139 | token.Wait() 140 | err := token.Error() 141 | if err != nil { 142 | close(d.uplink) 143 | d.uplink = nil 144 | } 145 | return d.uplink, err 146 | } 147 | 148 | func (d *devicePubSub) UnsubscribeUplink() error { 149 | d.Lock() 150 | defer d.Unlock() 151 | if d.uplink == nil { 152 | return nil 153 | } 154 | token := d.client.UnsubscribeDeviceUplink(d.appID, d.devID) 155 | token.Wait() 156 | close(d.uplink) 157 | d.uplink = nil 158 | return token.Error() 159 | } 160 | 161 | func (d *devicePubSub) SubscribeEvents() (<-chan *types.DeviceEvent, error) { 162 | if err := d.ctx.Err(); err != nil { 163 | return nil, err 164 | } 165 | d.Lock() 166 | defer d.Unlock() 167 | if d.events != nil { 168 | return d.events, nil 169 | } 170 | d.events = make(chan *types.DeviceEvent, mqttBufferSize) 171 | token := d.client.SubscribeDeviceEvents(d.appID, d.devID, "#", func(_ mqtt.Client, appID string, devID string, eventType types.EventType, payload []byte) { 172 | msg := types.DeviceEvent{ 173 | AppID: appID, 174 | DevID: devID, 175 | Event: eventType, 176 | } 177 | eventData := eventType.Data() 178 | if eventData != nil { 179 | if err := json.Unmarshal(payload, eventData); err == nil { 180 | msg.Data = eventData 181 | } 182 | } 183 | d.RLock() 184 | defer d.RUnlock() 185 | if d.events == nil { 186 | return 187 | } 188 | select { 189 | case d.events <- &msg: 190 | default: 191 | } 192 | }) 193 | token.Wait() 194 | err := token.Error() 195 | if err != nil { 196 | close(d.events) 197 | d.events = nil 198 | } 199 | return d.events, err 200 | } 201 | 202 | func (d *devicePubSub) UnsubscribeEvents() error { 203 | d.Lock() 204 | defer d.Unlock() 205 | if d.events == nil { 206 | return nil 207 | } 208 | token := d.client.UnsubscribeDeviceEvents(d.appID, d.devID, "#") 209 | token.Wait() 210 | close(d.events) 211 | d.events = nil 212 | return token.Error() 213 | } 214 | 215 | func (d *devicePubSub) SubscribeActivations() (<-chan *types.Activation, error) { 216 | if err := d.ctx.Err(); err != nil { 217 | return nil, err 218 | } 219 | d.Lock() 220 | defer d.Unlock() 221 | if d.activations != nil { 222 | return d.activations, nil 223 | } 224 | d.activations = make(chan *types.Activation, mqttBufferSize) 225 | token := d.client.SubscribeDeviceActivations(d.appID, d.devID, func(_ mqtt.Client, appID string, devID string, mqg types.Activation) { 226 | mqg.AppID = appID 227 | mqg.DevID = devID 228 | d.RLock() 229 | defer d.RUnlock() 230 | if d.activations == nil { 231 | return 232 | } 233 | select { 234 | case d.activations <- &mqg: 235 | default: 236 | } 237 | }) 238 | token.Wait() 239 | err := token.Error() 240 | if err != nil { 241 | close(d.activations) 242 | d.activations = nil 243 | } 244 | return d.activations, err 245 | } 246 | 247 | func (d *devicePubSub) UnsubscribeActivations() error { 248 | d.Lock() 249 | defer d.Unlock() 250 | if d.activations == nil { 251 | return nil 252 | } 253 | token := d.client.UnsubscribeDeviceActivations(d.appID, d.devID) 254 | token.Wait() 255 | close(d.activations) 256 | d.activations = nil 257 | return token.Error() 258 | } 259 | 260 | func (d *devicePubSub) Close() { 261 | d.cancel() 262 | } 263 | 264 | // ApplicationPubSub interface for publishing and subscribing to devices in an application 265 | type ApplicationPubSub interface { 266 | Publish(devID string, downlink *types.DownlinkMessage) error 267 | Device(devID string) DevicePubSub 268 | AllDevices() DeviceSub 269 | Close() 270 | } 271 | 272 | type applicationPubSub struct { 273 | logger log.Interface 274 | client mqtt.Client 275 | ctx context.Context 276 | cancel context.CancelFunc 277 | 278 | appID string 279 | } 280 | 281 | func (a *applicationPubSub) Device(devID string) DevicePubSub { 282 | d := &devicePubSub{ 283 | logger: a.logger, 284 | client: a.client, 285 | appID: a.appID, 286 | devID: devID, 287 | } 288 | d.ctx, d.cancel = context.WithCancel(a.ctx) 289 | go func() { 290 | <-d.ctx.Done() 291 | d.UnsubscribeUplink() 292 | d.UnsubscribeEvents() 293 | d.UnsubscribeActivations() 294 | }() 295 | return d 296 | } 297 | 298 | func (a *applicationPubSub) AllDevices() DeviceSub { 299 | return a.Device("+") 300 | } 301 | 302 | func (a *applicationPubSub) Publish(devID string, downlink *types.DownlinkMessage) error { 303 | d := &devicePubSub{ 304 | logger: a.logger, 305 | client: a.client, 306 | appID: a.appID, 307 | devID: devID, 308 | } 309 | return d.Publish(downlink) 310 | } 311 | 312 | func (a *applicationPubSub) Close() { 313 | a.cancel() 314 | } 315 | 316 | func (c *client) PubSub() (ApplicationPubSub, error) { 317 | if err := c.connectMQTT(); err != nil { 318 | return nil, err 319 | } 320 | if err := c.mqtt.ctx.Err(); err != nil { 321 | return nil, err 322 | } 323 | a := &applicationPubSub{ 324 | logger: c.Logger, 325 | client: c.mqtt.client, 326 | appID: c.appID, 327 | } 328 | a.ctx, a.cancel = context.WithCancel(c.mqtt.ctx) 329 | return a, nil 330 | } 331 | -------------------------------------------------------------------------------- /device_manager.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 The Things Network 2 | // Use of this source code is governed by the MIT license that can be found in the LICENSE file. 3 | 4 | package ttnsdk 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "time" 10 | 11 | "github.com/TheThingsNetwork/api/handler" 12 | "github.com/TheThingsNetwork/api/protocol/lorawan" 13 | "github.com/TheThingsNetwork/go-utils/grpc/ttnctx" 14 | "github.com/TheThingsNetwork/go-utils/log" 15 | "github.com/TheThingsNetwork/go-utils/random" 16 | "github.com/TheThingsNetwork/ttn/core/types" 17 | ) 18 | 19 | // DeviceManager manages devices within an application 20 | type DeviceManager interface { 21 | // List devices in an application. Use the limit and offset for pagination. Requests that fetch many devices will be 22 | // very slow, which is often not necessary. If you use this function too often, the response will be cached by the 23 | // server, and you might receive outdated data. 24 | List(limit, offset uint64) (DeviceList, error) 25 | 26 | // Get details for a device 27 | Get(devID string) (*Device, error) 28 | 29 | // Create or Update a device. 30 | Set(*Device) error 31 | 32 | // Delete a device 33 | Delete(devID string) error 34 | } 35 | 36 | // DeviceList is a slice of *SparseDevice. 37 | type DeviceList []*SparseDevice 38 | 39 | // AsDevices returns the DeviceList as a slice of *Device instead of *SparseDevice 40 | func (d DeviceList) AsDevices() []*Device { 41 | converted := make([]*Device, len(d)) 42 | for i, dev := range d { 43 | converted[i] = dev.AsDevice() 44 | } 45 | return converted 46 | } 47 | 48 | func (c *client) ManageDevices() (DeviceManager, error) { 49 | if err := c.connectHandler(); err != nil { 50 | return nil, err 51 | } 52 | return &deviceManager{ 53 | logger: c.Logger, 54 | client: handler.NewApplicationManagerClient(c.handler.conn), 55 | devAddrClient: lorawan.NewDevAddrManagerClient(c.handler.conn), 56 | getContext: c.getContext, 57 | requestTimeout: c.RequestTimeout, 58 | appID: c.appID, 59 | }, nil 60 | } 61 | 62 | type deviceManager struct { 63 | logger log.Interface 64 | client handler.ApplicationManagerClient 65 | devAddrClient lorawan.DevAddrManagerClient 66 | getContext func(context.Context) context.Context 67 | requestTimeout time.Duration 68 | 69 | appID string 70 | } 71 | 72 | func (d *deviceManager) List(limit, offset uint64) (devices DeviceList, err error) { 73 | ctx, cancel := context.WithTimeout(d.getContext(context.Background()), d.requestTimeout) 74 | defer cancel() 75 | ctx = ttnctx.OutgoingContextWithLimitAndOffset(ctx, limit, offset) 76 | res, err := d.client.GetDevicesForApplication(ctx, &handler.ApplicationIdentifier{AppID: d.appID}) 77 | if err != nil { 78 | return nil, err 79 | } 80 | for _, res := range res.Devices { 81 | dev := new(SparseDevice) 82 | dev.fromProto(res) 83 | devices = append(devices, dev) 84 | } 85 | return devices, nil 86 | } 87 | 88 | func (d *deviceManager) Get(devID string) (*Device, error) { 89 | ctx, cancel := context.WithTimeout(d.getContext(context.Background()), d.requestTimeout) 90 | defer cancel() 91 | res, err := d.client.GetDevice(ctx, &handler.DeviceIdentifier{AppID: d.appID, DevID: devID}) 92 | if err != nil { 93 | return nil, err 94 | } 95 | dev := &Device{deviceManager: d} 96 | dev.fromProto(res) 97 | return dev, nil 98 | } 99 | 100 | func (d *deviceManager) Set(dev *Device) error { 101 | if dev.AppID != d.appID { 102 | dev.AppID = d.appID 103 | } 104 | req := new(handler.Device) 105 | dev.toProto(req) 106 | ctx, cancel := context.WithTimeout(d.getContext(context.Background()), d.requestTimeout) 107 | defer cancel() 108 | _, err := d.client.SetDevice(ctx, req) // TODO: fill dev from response and set deviceManager when the server actually returns the device 109 | return err 110 | } 111 | 112 | func (d *deviceManager) Delete(devID string) error { 113 | ctx, cancel := context.WithTimeout(d.getContext(context.Background()), d.requestTimeout) 114 | defer cancel() 115 | _, err := d.client.DeleteDevice(ctx, &handler.DeviceIdentifier{AppID: d.appID, DevID: devID}) 116 | return err 117 | } 118 | 119 | // SparseDevice contains most, but not all fields of the device. It's returned by List operations to save server resources 120 | type SparseDevice struct { 121 | AppID string `json:"app_id"` 122 | DevID string `json:"dev_id"` 123 | AppEUI types.AppEUI `json:"app_eui"` 124 | DevEUI types.DevEUI `json:"dev_eui"` 125 | Description string `json:"description,omitempty"` 126 | DevAddr *types.DevAddr `json:"dev_addr,omitempty"` 127 | NwkSKey *types.NwkSKey `json:"nwk_s_key,omitempty"` 128 | AppSKey *types.AppSKey `json:"app_s_key,omitempty"` 129 | AppKey *types.AppKey `json:"app_key,omitempty"` 130 | Latitude float32 `json:"latitude,omitempty"` 131 | Longitude float32 `json:"longitude,omitempty"` 132 | Altitude int32 `json:"altitude,omitempty"` 133 | Attributes map[string]string `json:"attributes,omitempty"` 134 | } 135 | 136 | func (d *SparseDevice) fromProto(dev *handler.Device) { 137 | d.AppID = dev.GetAppID() 138 | d.DevID = dev.GetDevID() 139 | d.Description = dev.Description 140 | if lorawanDevice := dev.GetLoRaWANDevice(); lorawanDevice != nil { 141 | d.AppEUI = lorawanDevice.GetAppEUI() 142 | d.DevEUI = lorawanDevice.GetDevEUI() 143 | d.DevAddr = lorawanDevice.DevAddr 144 | d.NwkSKey = lorawanDevice.NwkSKey 145 | d.AppSKey = lorawanDevice.AppSKey 146 | d.AppKey = lorawanDevice.AppKey 147 | } 148 | d.Latitude = dev.Latitude 149 | d.Longitude = dev.Longitude 150 | d.Altitude = dev.Altitude 151 | d.Attributes = dev.Attributes 152 | } 153 | 154 | func (d *SparseDevice) toProto(dev *handler.Device) { 155 | dev.AppID = d.AppID 156 | dev.DevID = d.DevID 157 | dev.Description = d.Description 158 | dev.Latitude = d.Latitude 159 | dev.Longitude = d.Longitude 160 | dev.Altitude = d.Altitude 161 | dev.Attributes = d.Attributes 162 | if dev.Device == nil { 163 | dev.Device = &handler.Device_LoRaWANDevice{LoRaWANDevice: &lorawan.Device{}} 164 | } 165 | lorawanDevice := dev.GetLoRaWANDevice() 166 | lorawanDevice.AppID = d.AppID 167 | lorawanDevice.DevID = d.DevID 168 | lorawanDevice.AppEUI = d.AppEUI 169 | lorawanDevice.DevEUI = d.DevEUI 170 | lorawanDevice.DevAddr = d.DevAddr 171 | lorawanDevice.NwkSKey = d.NwkSKey 172 | lorawanDevice.AppSKey = d.AppSKey 173 | lorawanDevice.AppKey = d.AppKey 174 | } 175 | 176 | // AsDevice wraps the *SparseDevice and returns a *Device containing that sparse device 177 | func (d *SparseDevice) AsDevice() *Device { 178 | if d == nil { 179 | return nil 180 | } 181 | return &Device{SparseDevice: *d} 182 | } 183 | 184 | // Device in an application 185 | type Device struct { 186 | deviceManager DeviceManager 187 | 188 | SparseDevice 189 | FCntUp uint32 `json:"f_cnt_up"` 190 | FCntDown uint32 `json:"f_cnt_down"` 191 | DisableFCntCheck bool `json:"disable_f_cnt_check"` 192 | Uses32BitFCnt bool `json:"uses32_bit_f_cnt"` 193 | ActivationConstraints string `json:"activation_constraints"` 194 | LastSeen time.Time `json:"last_seen"` 195 | } 196 | 197 | func (d *Device) addActivationConstraint(c string) { 198 | constraints := strings.Split(d.ActivationConstraints, ",") 199 | for _, constraint := range constraints { 200 | if constraint == c { 201 | return 202 | } 203 | } 204 | constraints = append(constraints, c) 205 | d.ActivationConstraints = strings.Join(constraints, ",") 206 | } 207 | 208 | // IsNew indicates whether the device is new. 209 | func (d *Device) IsNew() bool { return d.deviceManager == nil } 210 | 211 | // SetManager sets the manager of the device. This function panics if this is not a new device. 212 | func (d *Device) SetManager(manager DeviceManager) { 213 | if d.deviceManager == manager { 214 | return 215 | } 216 | if !d.IsNew() { 217 | panic("ttn-sdk: you can not change the device manager") 218 | } 219 | d.deviceManager = manager 220 | } 221 | 222 | // Update the device. This function panics if this is a new device. 223 | func (d *Device) Update() error { 224 | if d.IsNew() { 225 | panic("ttn-sdk: you can not update new devices") 226 | } 227 | return d.deviceManager.Set(d) 228 | } 229 | 230 | // Delete the device. This function panics if this is a new device. 231 | func (d *Device) Delete() error { 232 | if d.IsNew() { 233 | panic("ttn-sdk: you can not update new devices") 234 | } 235 | return d.deviceManager.Delete(d.DevID) 236 | } 237 | 238 | // PersonalizeRandom personalizes a device by requesting a DevAddr from the network, and setting the NwkSKey and AppSKey 239 | // to randomly generated values. This function panics if this is a new device, so make sure you Get() the device first. 240 | func (d *Device) PersonalizeRandom() error { 241 | return d.PersonalizeFunc(func(_ types.DevAddr) (nwkSKey types.NwkSKey, appSKey types.AppSKey) { 242 | random.FillBytes(nwkSKey[:]) 243 | random.FillBytes(appSKey[:]) 244 | return 245 | }) 246 | } 247 | 248 | // Personalize a device by requesting a DevAddr from the network, and setting the NwkSKey and AppSKey to the given values. 249 | // This function panics if this is a new device, so make sure you Get() the device first. 250 | func (d *Device) Personalize(nwkSKey types.NwkSKey, appSKey types.AppSKey) error { 251 | return d.PersonalizeFunc(func(_ types.DevAddr) (types.NwkSKey, types.AppSKey) { 252 | return nwkSKey, appSKey 253 | }) 254 | } 255 | 256 | // PersonalizeFunc personalizes a device by requesting a DevAddr from the network, and setting the NwkSKey and AppSKey 257 | // to the result of the personalizeFunc. This function panics if this is a new device, so make sure you Get() the device 258 | // first. 259 | func (d *Device) PersonalizeFunc(personalizeFunc func(types.DevAddr) (types.NwkSKey, types.AppSKey)) error { 260 | if d.IsNew() { 261 | panic("ttn-sdk: you can not update new devices. Use the Get() function to retrieve the device from the server first.") 262 | } 263 | manager, ok := d.deviceManager.(*deviceManager) 264 | if !ok { 265 | panic("ttn-sdk: you can only personalize devices on The Things Network") 266 | } 267 | d.addActivationConstraint("abp") 268 | ctx, cancel := context.WithTimeout(manager.getContext(context.Background()), manager.requestTimeout) 269 | defer cancel() 270 | res, err := manager.devAddrClient.GetDevAddr(ctx, &lorawan.DevAddrRequest{Usage: strings.Split(d.ActivationConstraints, ",")}) 271 | if err != nil { 272 | return err 273 | } 274 | d.DevAddr = &res.DevAddr 275 | nwkSKey, appSKey := personalizeFunc(res.DevAddr) 276 | d.NwkSKey, d.AppSKey = &nwkSKey, &appSKey 277 | return d.Update() 278 | } 279 | 280 | func (d *Device) fromProto(dev *handler.Device) { 281 | d.SparseDevice.fromProto(dev) 282 | if lorawanDevice := dev.GetLoRaWANDevice(); lorawanDevice != nil { 283 | d.FCntUp = lorawanDevice.FCntUp 284 | d.FCntDown = lorawanDevice.FCntDown 285 | d.DisableFCntCheck = lorawanDevice.DisableFCntCheck 286 | d.Uses32BitFCnt = lorawanDevice.Uses32BitFCnt 287 | d.ActivationConstraints = lorawanDevice.ActivationConstraints 288 | d.LastSeen = time.Unix(0, lorawanDevice.LastSeen) 289 | } 290 | } 291 | 292 | func (d *Device) toProto(dev *handler.Device) { 293 | d.SparseDevice.toProto(dev) 294 | lorawanDevice := dev.GetLoRaWANDevice() 295 | lorawanDevice.FCntUp = d.FCntUp 296 | lorawanDevice.FCntDown = d.FCntDown 297 | lorawanDevice.DisableFCntCheck = d.DisableFCntCheck 298 | lorawanDevice.Uses32BitFCnt = d.Uses32BitFCnt 299 | lorawanDevice.ActivationConstraints = d.ActivationConstraints 300 | } 301 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | import ttnsdk "github.com/TheThingsNetwork/go-app-sdk" 4 | 5 | Package ttnsdk implements the Go SDK for The Things Network. 6 | 7 | This package wraps The Things Network's application and device management APIs 8 | (github.com/TheThingsNetwork/api) and the publish/subscribe API 9 | (github.com/TheThingsNetwork/ttn/mqtt). It works with the Discovery Server to 10 | retrieve the addresses of the Handler and MQTT server. 11 | 12 | ## Variables 13 | 14 | ```go 15 | var ClientVersion = "2.x.x" 16 | ``` 17 | ClientVersion to use 18 | 19 | ```go 20 | var DialOptions = []grpc.DialOption{ 21 | grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient( 22 | rpclog.UnaryClientInterceptor(nil), 23 | )), 24 | grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient( 25 | restartstream.Interceptor(restartstream.DefaultSettings), 26 | rpclog.StreamClientInterceptor(nil), 27 | )), 28 | grpc.WithBlock(), 29 | } 30 | ``` 31 | DialOptions to use when connecting to components 32 | 33 | ## func MoveDevice 34 | 35 | ```go 36 | func MoveDevice(devID string, from, to DeviceManager) (err error) 37 | ``` 38 | MoveDevice moves a device to another application 39 | 40 | ## type ApplicationManager 41 | 42 | ```go 43 | type ApplicationManager interface { 44 | // Get the payload format used in this application. If the payload format is "custom", you can get the custom JS 45 | // payload functions with the GetCustomPayloadFunctions() function. 46 | GetPayloadFormat() (string, error) 47 | 48 | // Set the payload format to use in this application. If you want to use custom JS payload functions, use the 49 | // SetCustomPayloadFunctions() function instead. If you want to disable payload conversion, pass an empty string. 50 | SetPayloadFormat(format string) error 51 | 52 | // Get the custom JS payload functions. 53 | GetCustomPayloadFunctions() (jsDecoder, jsConverter, jsValidator, jsEncoder string, err error) 54 | 55 | // Set the custom JS payload functions. 56 | // 57 | // Example Decoder: 58 | // 59 | // // Decoder (Array, uint8) returns (Object) 60 | // function Decoder(bytes, port) { 61 | // var decoded = {}; 62 | // return decoded; 63 | // } 64 | // 65 | // Example Converter: 66 | // 67 | // // Converter (Object, uint8) returns (Object) 68 | // function Converter(decoded, port) { 69 | // var converted = decoded; 70 | // return converted; 71 | // } 72 | // 73 | // Example Validator: 74 | // // Validator (Object, uint8) returns (Boolean) 75 | // function Validator(converted, port) { 76 | // return true; 77 | // } 78 | // 79 | // Example Encoder: 80 | // 81 | // // Validator (Object, uint8) returns (Array) 82 | // function Encoder(object, port) { 83 | // var bytes = []; 84 | // return bytes; 85 | // } 86 | SetCustomPayloadFunctions(jsDecoder, jsConverter, jsValidator, jsEncoder string) error 87 | } 88 | ``` 89 | 90 | ApplicationManager manages an application. 91 | 92 | ## type ApplicationPubSub 93 | 94 | ```go 95 | type ApplicationPubSub interface { 96 | Publish(devID string, downlink *types.DownlinkMessage) error 97 | Device(devID string) DevicePubSub 98 | AllDevices() DeviceSub 99 | Close() 100 | } 101 | ``` 102 | 103 | ApplicationPubSub interface for publishing and subscribing to devices in an 104 | application 105 | 106 | ## type Client 107 | 108 | ```go 109 | type Client interface { 110 | // Close the client and clean up all connections 111 | Close() error 112 | 113 | // Subscribe to uplink and events, publish downlink 114 | PubSub() (ApplicationPubSub, error) 115 | 116 | // Manage the application 117 | ManageApplication() (ApplicationManager, error) 118 | 119 | // Manage devices in the application 120 | ManageDevices() (DeviceManager, error) 121 | 122 | // Simulate uplink messages for a device (for testing) 123 | Simulate(devID string) (Simulator, error) 124 | } 125 | ``` 126 | 127 | Client interface for The Things Network's API. 128 | 129 | ## type ClientConfig 130 | 131 | ```go 132 | type ClientConfig struct { 133 | Logger log.Interface 134 | 135 | // The name of this client 136 | ClientName string 137 | 138 | // The version of this client (in the default config, this is the value of ttnsdk.ClientVersion) 139 | ClientVersion string 140 | 141 | // TLS Configuration only has to be set if connecting with servers that do not have trusted certificates. 142 | TLSConfig *tls.Config 143 | 144 | // Address of the Account Server (in the default config, this is https://account.thethingsnetwork.org) 145 | AccountServerAddress string 146 | 147 | // Client ID for the account server (if you registered your client) 148 | AccountServerClientID string 149 | 150 | // Client Secret for the account server (if you registered your client) 151 | AccountServerClientSecret string 152 | 153 | // Address of the Discovery Server (in the default config, this is discovery.thethings.network:1900) 154 | DiscoveryServerAddress string 155 | 156 | // Set this to true if the Discovery Server is insecure (not recommended) 157 | DiscoveryServerInsecure bool 158 | 159 | // Address of the Handler (optional) 160 | HandlerAddress string 161 | 162 | // Timeout for requests (in the default config, this is 10 seconds) 163 | RequestTimeout time.Duration 164 | } 165 | ``` 166 | 167 | ClientConfig contains the configuration for the API client. Use the NewConfig() 168 | or NewCommunityConfig() functions to initialize your configuration, otherwise 169 | NewClient will panic. 170 | 171 | ## func NewCommunityConfig 172 | 173 | ```go 174 | func NewCommunityConfig(clientName string) ClientConfig 175 | ``` 176 | NewCommunityConfig creates a new configuration for the API client that is 177 | pre-configured for the Public Community Network. 178 | 179 | ## func NewConfig 180 | 181 | ```go 182 | func NewConfig(clientName, accountServerAddress, discoveryServerAddress string) ClientConfig 183 | ``` 184 | NewConfig creates a new configuration for the API client. 185 | 186 | ## func (ClientConfig) NewClient 187 | 188 | ```go 189 | func (c ClientConfig) NewClient(appID, appAccessKey string) Client 190 | ``` 191 | NewClient creates a new API client from the configuration, using the given 192 | Application ID and Application access key. 193 | 194 | ## type Device 195 | 196 | ```go 197 | type Device struct { 198 | SparseDevice 199 | FCntUp uint32 `json:"f_cnt_up"` 200 | FCntDown uint32 `json:"f_cnt_down"` 201 | DisableFCntCheck bool `json:"disable_f_cnt_check"` 202 | Uses32BitFCnt bool `json:"uses32_bit_f_cnt"` 203 | ActivationConstraints string `json:"activation_constraints"` 204 | LastSeen time.Time `json:"last_seen"` 205 | } 206 | ``` 207 | 208 | Device in an application 209 | 210 | ## func (*Device) Delete 211 | 212 | ```go 213 | func (d *Device) Delete() error 214 | ``` 215 | Delete the device. This function panics if this is a new device. 216 | 217 | ## func (*Device) IsNew 218 | 219 | ```go 220 | func (d *Device) IsNew() bool 221 | ``` 222 | IsNew indicates whether the device is new. 223 | 224 | ## func (*Device) Personalize 225 | 226 | ```go 227 | func (d *Device) Personalize(nwkSKey types.NwkSKey, appSKey types.AppSKey) error 228 | ``` 229 | Personalize a device by requesting a DevAddr from the network, and setting the 230 | NwkSKey and AppSKey to the given values. This function panics if this is a new 231 | device, so make sure you Get() the device first. 232 | 233 | ## func (*Device) PersonalizeFunc 234 | 235 | ```go 236 | func (d *Device) PersonalizeFunc(personalizeFunc func(types.DevAddr) (types.NwkSKey, types.AppSKey)) error 237 | ``` 238 | PersonalizeFunc personalizes a device by requesting a DevAddr from the network, 239 | and setting the NwkSKey and AppSKey to the result of the personalizeFunc. This 240 | function panics if this is a new device, so make sure you Get() the device 241 | first. 242 | 243 | ## func (*Device) PersonalizeRandom 244 | 245 | ```go 246 | func (d *Device) PersonalizeRandom() error 247 | ``` 248 | PersonalizeRandom personalizes a device by requesting a DevAddr from the 249 | network, and setting the NwkSKey and AppSKey to randomly generated values. This 250 | function panics if this is a new device, so make sure you Get() the device 251 | first. 252 | 253 | ## func (*Device) SetManager 254 | 255 | ```go 256 | func (d *Device) SetManager(manager DeviceManager) 257 | ``` 258 | SetManager sets the manager of the device. This function panics if this is not a 259 | new device. 260 | 261 | ## func (*Device) Update 262 | 263 | ```go 264 | func (d *Device) Update() error 265 | ``` 266 | Update the device. This function panics if this is a new device. 267 | 268 | ## type DeviceList 269 | 270 | ```go 271 | type DeviceList []*SparseDevice 272 | ``` 273 | 274 | DeviceList is a slice of *SparseDevice. 275 | 276 | ## func (DeviceList) AsDevices 277 | 278 | ```go 279 | func (d DeviceList) AsDevices() []*Device 280 | ``` 281 | AsDevices returns the DeviceList as a slice of *Device instead of *SparseDevice 282 | 283 | ## type DeviceManager 284 | 285 | ```go 286 | type DeviceManager interface { 287 | // List devices in an application. Use the limit and offset for pagination. Requests that fetch many devices will be 288 | // very slow, which is often not necessary. If you use this function too often, the response will be cached by the 289 | // server, and you might receive outdated data. 290 | List(limit, offset uint64) (DeviceList, error) 291 | 292 | // Get details for a device 293 | Get(devID string) (*Device, error) 294 | 295 | // Create or Update a device. 296 | Set(*Device) error 297 | 298 | // Delete a device 299 | Delete(devID string) error 300 | } 301 | ``` 302 | 303 | DeviceManager manages devices within an application 304 | 305 | ## type DevicePub 306 | 307 | ```go 308 | type DevicePub interface { 309 | Publish(*types.DownlinkMessage) error 310 | } 311 | ``` 312 | 313 | DevicePub interface for publishing downlink messages to the device 314 | 315 | ## type DevicePubSub 316 | 317 | ```go 318 | type DevicePubSub interface { 319 | DevicePub 320 | DeviceSub 321 | } 322 | ``` 323 | 324 | DevicePubSub combines the DevicePub and DeviceSub interfaces 325 | 326 | ## type DeviceSub 327 | 328 | ```go 329 | type DeviceSub interface { 330 | SubscribeUplink() (<-chan *types.UplinkMessage, error) 331 | UnsubscribeUplink() error 332 | SubscribeEvents() (<-chan *types.DeviceEvent, error) 333 | UnsubscribeEvents() error 334 | SubscribeActivations() (<-chan *types.Activation, error) 335 | UnsubscribeActivations() error 336 | Close() 337 | } 338 | ``` 339 | 340 | DeviceSub interface for subscribing to uplink messages and events from the 341 | device 342 | 343 | ## type Simulator 344 | 345 | ```go 346 | type Simulator interface { 347 | Uplink(port uint8, payload []byte) error 348 | } 349 | ``` 350 | 351 | Simulator simulates messages for devices 352 | 353 | ## type SparseDevice 354 | 355 | ```go 356 | type SparseDevice struct { 357 | AppID string `json:"app_id"` 358 | DevID string `json:"dev_id"` 359 | AppEUI types.AppEUI `json:"app_eui"` 360 | DevEUI types.DevEUI `json:"dev_eui"` 361 | Description string `json:"description,omitempty"` 362 | DevAddr *types.DevAddr `json:"dev_addr,omitempty"` 363 | NwkSKey *types.NwkSKey `json:"nwk_s_key,omitempty"` 364 | AppSKey *types.AppSKey `json:"app_s_key,omitempty"` 365 | AppKey *types.AppKey `json:"app_key,omitempty"` 366 | Latitude float32 `json:"latitude,omitempty"` 367 | Longitude float32 `json:"longitude,omitempty"` 368 | Altitude int32 `json:"altitude,omitempty"` 369 | Attributes map[string]string `json:"attributes,omitempty"` 370 | } 371 | ``` 372 | 373 | SparseDevice contains most, but not all fields of the device. It's returned by 374 | List operations to save server resources 375 | 376 | ## func (*SparseDevice) AsDevice 377 | 378 | ```go 379 | func (d *SparseDevice) AsDevice() *Device 380 | ``` 381 | AsDevice wraps the *SparseDevice and returns a *Device containing that sparse 382 | device 383 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/TheThingsNetwork/api v0.0.0-20190330165854-3fb363b63d07/go.mod h1:iGcCXFi0My4IJbLSWWlRONaYiMtSjLbdkfFE5xBrgRc= 4 | github.com/TheThingsNetwork/api v0.0.0-20190516111443-a3523f89e84f h1:socqGNa6e5LZ4mQFZmwKHrejRHX3geh6kZzHUhVCSqI= 5 | github.com/TheThingsNetwork/api v0.0.0-20190516111443-a3523f89e84f/go.mod h1:MYWz0xDXK+4LWQMnAcm//UPhLoamf67LgFjMwUdhKbE= 6 | github.com/TheThingsNetwork/go-account-lib v0.0.0-20190516094738-77d15a3f8875 h1:Q6gqExgAk2mQlZPur7dwbGsy9Uxs9Hs2kurMTd+o2Rc= 7 | github.com/TheThingsNetwork/go-account-lib v0.0.0-20190516094738-77d15a3f8875/go.mod h1:VZeXL6kkGnZouPnLESpVSGSRQNlz8zAOKHzr0P6m4pc= 8 | github.com/TheThingsNetwork/go-utils v0.0.0-20190516083235-bdd4967fab4e h1:JEt3G2ONKGfW1YDp2A8Q/+kZIuIB117nkit0+GDJN04= 9 | github.com/TheThingsNetwork/go-utils v0.0.0-20190516083235-bdd4967fab4e/go.mod h1:9uzg7Jk8ywYqL+xUEhTNrJcs68Nafj4qTaz/zB+STwg= 10 | github.com/TheThingsNetwork/ttn/api v0.0.0-20190516081709-034d40b328bd h1:vCjDYImJDdW+39EXwij00yzDi1pd3TmP6XtCteDJBd0= 11 | github.com/TheThingsNetwork/ttn/api v0.0.0-20190516081709-034d40b328bd/go.mod h1:UCRXmEaShvS/wHOf2RcoY2vKUGJnrYrotBA6LZzYdFM= 12 | github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516081709-034d40b328bd/go.mod h1:VVWTaeAJHezuE+c0Vk0AJ4R6KSLg50H1y3RB7vGhGOA= 13 | github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516092602-86414c703ee1/go.mod h1:q3r/3g5fixboWUOCounAWo+Y8OlkKdhVnGosLMEQ09Q= 14 | github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516112328-fcd38e2b9dc6 h1:XUWO3mw11jfff7qjDR2Z9VxMofsFDyrxUhPY/82lrqg= 15 | github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516112328-fcd38e2b9dc6/go.mod h1:q3r/3g5fixboWUOCounAWo+Y8OlkKdhVnGosLMEQ09Q= 16 | github.com/TheThingsNetwork/ttn/mqtt v0.0.0-20190516112328-fcd38e2b9dc6 h1:2T7NTSemIJ9uWv0VcfxS7ANkFvRHqQ7+yXS0OQZCiy8= 17 | github.com/TheThingsNetwork/ttn/mqtt v0.0.0-20190516112328-fcd38e2b9dc6/go.mod h1:reZ1DGzwREWfUapXIyMWU4Ybj8yxB3DD6zBW5Onm2Z0= 18 | github.com/TheThingsNetwork/ttn/utils/errors v0.0.0-20190516081709-034d40b328bd h1:ITXOJpmUR4Jhp3Xb/xNUIJH4WR0h2/NsxZkSDzFIFiU= 19 | github.com/TheThingsNetwork/ttn/utils/errors v0.0.0-20190516081709-034d40b328bd/go.mod h1:e8FjzgvhAVx9+iqPloB4v7QM0rmv+r5ysRn9kWFamG4= 20 | github.com/TheThingsNetwork/ttn/utils/random v0.0.0-20190516081709-034d40b328bd/go.mod h1:ktVq1/rYkTlgilBixqCtltTh29rOUnZET4g50xoKlpE= 21 | github.com/TheThingsNetwork/ttn/utils/random v0.0.0-20190516092602-86414c703ee1 h1:Y0jKI253ear5Kz8XJf3PIv2+rtHB2b1UoWDIpm8bdV0= 22 | github.com/TheThingsNetwork/ttn/utils/random v0.0.0-20190516092602-86414c703ee1/go.mod h1:ktVq1/rYkTlgilBixqCtltTh29rOUnZET4g50xoKlpE= 23 | github.com/TheThingsNetwork/ttn/utils/security v0.0.0-20190516081709-034d40b328bd h1:og10Wq5S/QC+f4ziON4vrxlYKv9gfEKxG8v/MDs00xw= 24 | github.com/TheThingsNetwork/ttn/utils/security v0.0.0-20190516081709-034d40b328bd/go.mod h1:aaYF12LufW5Xs4Z2C6UOrCzpkyoBjw+rmHCcNYgb1JU= 25 | github.com/TheThingsNetwork/ttn/utils/testing v0.0.0-20190516092602-86414c703ee1 h1:R/9yZMe1E5BhUuKhxFOZp/s5rIfg8itNDsRAVgMm1ro= 26 | github.com/TheThingsNetwork/ttn/utils/testing v0.0.0-20190516092602-86414c703ee1/go.mod h1:KE2xKKLXyXmH9KBkUWJD5XPI0K10T2tj7E1kBLHq8kQ= 27 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 28 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 29 | github.com/apex/log v1.1.0 h1:J5rld6WVFi6NxA6m8GJ1LJqu3+GiTFIt3mYv27gdQWI= 30 | github.com/apex/log v1.1.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY= 31 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= 32 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 33 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 34 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= 35 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 36 | github.com/bluele/gcache v0.0.0-20190301044115-79ae3b2d8680/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk= 37 | github.com/brocaar/lorawan v0.0.0-20170626123636-a64aca28516d h1:yIe4YHMG4WUoBCWlcDsfFD1IgnysKNLDKCe+yY0sknc= 38 | github.com/brocaar/lorawan v0.0.0-20170626123636-a64aca28516d/go.mod h1:kwUChfPyeHBQumTUYBvOkO4prdwMM55wby9Zw9lhzlA= 39 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 44 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 45 | github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= 46 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 47 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 48 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 49 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 50 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 51 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 52 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 53 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 54 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 55 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 56 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 57 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 58 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 59 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 60 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 61 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 62 | github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= 63 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 64 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 66 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 68 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= 69 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 70 | github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976/go.mod h1:ZGQeOwybjD8lkCjIyJfqR5LD2wMVHJ31d6GdPxoTsWY= 71 | github.com/gotnospirit/messageformat v0.0.0-20180622080451-0eab1176a3fb/go.mod h1:NO9UUa4C4cSmRsYSfZMAKhI5ifCRzOjSGe/pi7TKRvs= 72 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 73 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 74 | github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI= 75 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 76 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 77 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 78 | github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= 79 | github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY= 80 | github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU= 81 | github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= 82 | github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd/go.mod h1:TlmyIZDpGmwRoTWiakdr+HA1Tukze6C6XbRVidYq02M= 83 | github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff h1:2xRHTvkpJ5zJmglXLRqHiZQNjUoOkhUyhTAhEQvPAWw= 84 | github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff/go.mod h1:gJWba/XXGl0UoOmBQKRWCJdHrr3nE0T65t6ioaj3mLI= 85 | github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ulwVIHVDWFHp8eIXmbfSExkvdn9qMXI= 86 | github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11/go.mod h1:+DBdDyfoO2McrOyDemRBq0q9CMEByef7sYl7JH5Q3BI= 87 | github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb h1:uSWBjJdMf47kQlXMwWEfmc864bA1wAC+Kl3ApryuG9Y= 88 | github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb/go.mod h1:ivcmUvxXWjb27NsPEaiYK7AidlZXS7oQ5PowUS9z3I4= 89 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 90 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 91 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 92 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 93 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 94 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 95 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 96 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 97 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 98 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 99 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 100 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 101 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 102 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 103 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 104 | github.com/mwitkow/go-grpc-middleware v1.0.0 h1:XraEe8LhUuB33YeV4NWfLh2KUZicskSZ2lMhVRnDvTQ= 105 | github.com/mwitkow/go-grpc-middleware v1.0.0/go.mod h1:wqm8af53+/cILryTaG+dCJS6CsDMVZDxlKh6lSkF19U= 106 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 107 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 108 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 109 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 110 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 111 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 112 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 113 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 114 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 115 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 116 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 117 | github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= 118 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 119 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 120 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 121 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 122 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 123 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= 124 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 125 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 126 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 127 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= 128 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 129 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 130 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 131 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 132 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 133 | github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 h1:hBSHahWMEgzwRyS6dRpxY0XyjZsHyQ61s084wo5PJe0= 134 | github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 135 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 136 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= 137 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 138 | github.com/smartystreets/gunit v0.0.0-20190426220047-d9c9211acd48/go.mod h1:oqKsUQaUkJ2EU1ZzLQFJt1WUp9DDuj1CnZbp4DwPwL4= 139 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 140 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 141 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 142 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 143 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 144 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 145 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 146 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 147 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 148 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 149 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 150 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 151 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 152 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 153 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 154 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 155 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 156 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 157 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 158 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 159 | golang.org/x/net v0.0.0-20190514140710-3ec191127204 h1:4yG6GqBtw9C+UrLp6s2wtSniayy/Vd/3F7ffLE427XI= 160 | golang.org/x/net v0.0.0-20190514140710-3ec191127204/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 161 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 162 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 163 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 169 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 170 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 172 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20190516014833-cab07311ab81/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20190516110030-61b9204099cb h1:k07iPOt0d6nEnwXF+kHB+iEg+WSuKe/SOQuFM2QoD+E= 175 | golang.org/x/sys v0.0.0-20190516110030-61b9204099cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 177 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 178 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 179 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 180 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 181 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 182 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 183 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 184 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 185 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 186 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 187 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 188 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 189 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 190 | google.golang.org/genproto v0.0.0-20190515210553-995ef27e003f h1:1ByLH6hSgeVhrIAQ+Y/J0kT6T3sROUx7eH9qY4UBQh4= 191 | google.golang.org/genproto v0.0.0-20190515210553-995ef27e003f/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 192 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 193 | google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= 194 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 195 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 198 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 199 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 200 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 201 | gopkg.in/redis.v5 v5.2.9 h1:MNZYOLPomQzZMfpN3ZtD1uyJ2IDonTTlxYiV/pEApiw= 202 | gopkg.in/redis.v5 v5.2.9/go.mod h1:6gtv0/+A4iM08kdRfocWYB3bLX2tebpNtfKlFT6H4mY= 203 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 204 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 205 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 206 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 207 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 209 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 210 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 211 | --------------------------------------------------------------------------------