diff --git a/CHANGELOG.md b/CHANGELOG.md index ce900d58e0..f05d774e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ For details about compatibility between different releases, see the **Commitment - Option to filter out non-gateway related frequency plans. - `ListFrequencyPlans` RPC has a new `gateways-only` flag. - Option to pause application webhooks. +- Endpoint for claiming gateways using a qr code ### Changed diff --git a/api/ttn/lorawan/v3/api.md b/api/ttn/lorawan/v3/api.md index 6386fa8517..cba83db331 100644 --- a/api/ttn/lorawan/v3/api.md +++ b/api/ttn/lorawan/v3/api.md @@ -743,10 +743,13 @@ - [Message `GetQRCodeFormatRequest`](#ttn.lorawan.v3.GetQRCodeFormatRequest) - [Message `ParseEndDeviceQRCodeRequest`](#ttn.lorawan.v3.ParseEndDeviceQRCodeRequest) - [Message `ParseEndDeviceQRCodeResponse`](#ttn.lorawan.v3.ParseEndDeviceQRCodeResponse) + - [Message `ParseGatewayQRCodeRequest`](#ttn.lorawan.v3.ParseGatewayQRCodeRequest) + - [Message `ParseGatewayQRCodeResponse`](#ttn.lorawan.v3.ParseGatewayQRCodeResponse) - [Message `QRCodeFormat`](#ttn.lorawan.v3.QRCodeFormat) - [Message `QRCodeFormats`](#ttn.lorawan.v3.QRCodeFormats) - [Message `QRCodeFormats.FormatsEntry`](#ttn.lorawan.v3.QRCodeFormats.FormatsEntry) - [Service `EndDeviceQRCodeGenerator`](#ttn.lorawan.v3.EndDeviceQRCodeGenerator) + - [Service `GatewayQRCodeGenerator`](#ttn.lorawan.v3.GatewayQRCodeGenerator) - [File `ttn/lorawan/v3/regional.proto`](#ttn/lorawan/v3/regional.proto) - [Message `ConcentratorConfig`](#ttn.lorawan.v3.ConcentratorConfig) - [Message `ConcentratorConfig.Channel`](#ttn.lorawan.v3.ConcentratorConfig.Channel) @@ -10542,6 +10545,34 @@ The Pba service allows clients to manage peering through Packet Broker. | `format_id` | [`string`](#string) | | Identifier of the format used to successfully parse the QR code data. | | `end_device_template` | [`EndDeviceTemplate`](#ttn.lorawan.v3.EndDeviceTemplate) | | | +### Message `ParseGatewayQRCodeRequest` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `format_id` | [`string`](#string) | | QR code format identifier. If this field is not specified, the server will default to ttigpro1. | +| `qr_code` | [`bytes`](#bytes) | | Raw QR code contents. | + +#### Field Rules + +| Field | Validations | +| ----- | ----------- | +| `format_id` |

`string.max_len`: `36`

`string.pattern`: `^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$`

| +| `qr_code` |

`bytes.min_len`: `10`

`bytes.max_len`: `1024`

| + +### Message `ParseGatewayQRCodeResponse` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `format_id` | [`string`](#string) | | Identifier of the format used to parse the QR code data. | +| `gateway_eui` | [`bytes`](#bytes) | | | +| `owner_token` | [`string`](#string) | | | + +#### Field Rules + +| Field | Validations | +| ----- | ----------- | +| `gateway_eui` |

`bytes.len`: `8`

| + ### Message `QRCodeFormat` | Field | Type | Label | Description | @@ -10597,6 +10628,21 @@ The EndDeviceQRCodeGenerator service provides functionality to generate and pars | `Parse` | `POST` | `/api/v3/qr-codes/end-devices/parse` | `*` | | `Parse` | `POST` | `/api/v3/qr-codes/end-devices/{format_id}/parse` | `*` | +### Service `GatewayQRCodeGenerator` + +The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways. + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +| `Parse` | [`ParseGatewayQRCodeRequest`](#ttn.lorawan.v3.ParseGatewayQRCodeRequest) | [`ParseGatewayQRCodeResponse`](#ttn.lorawan.v3.ParseGatewayQRCodeResponse) | Parse QR Codes of known formats and return the information contained within. | + +#### HTTP bindings + +| Method Name | Method | Pattern | Body | +| ----------- | ------ | ------- | ---- | +| `Parse` | `POST` | `/api/v3/qr-codes/gateways/parse` | `*` | +| `Parse` | `POST` | `/api/v3/qr-codes/gateways/{format_id}/parse` | `*` | + ## File `ttn/lorawan/v3/regional.proto` ### Message `ConcentratorConfig` diff --git a/api/ttn/lorawan/v3/api.swagger.json b/api/ttn/lorawan/v3/api.swagger.json index dfdc12b208..77b09c66c7 100644 --- a/api/ttn/lorawan/v3/api.swagger.json +++ b/api/ttn/lorawan/v3/api.swagger.json @@ -245,6 +245,9 @@ "name": "EndDeviceQRCodeGenerator", "description": "Generate and parse end device QR codes." }, + { + "name": "GatewayQRCodeGenerator" + }, { "name": "EntityRegistrySearch", "description": "Search for entities in the Entity Registry." @@ -13142,7 +13145,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/EndDeviceQRCodeGeneratorParseBody" + "$ref": "#/definitions/v3EndDeviceQRCodeGeneratorParseBody" } } ], @@ -13151,6 +13154,79 @@ ] } }, + "/qr-codes/gateways/parse": { + "post": { + "summary": "Parse QR Codes of known formats and return the information contained within.", + "operationId": "GatewayQRCodeGenerator_Parse", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3ParseGatewayQRCodeResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v3ParseGatewayQRCodeRequest" + } + } + ], + "tags": [ + "GatewayQRCodeGenerator" + ] + } + }, + "/qr-codes/gateways/{format_id}/parse": { + "post": { + "summary": "Parse QR Codes of known formats and return the information contained within.", + "operationId": "GatewayQRCodeGenerator_Parse2", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v3ParseGatewayQRCodeResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/googlerpcStatus" + } + } + }, + "parameters": [ + { + "name": "format_id", + "description": "QR code format identifier.\nIf this field is not specified, the server will default to ttigpro1.", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v3GatewayQRCodeGeneratorParseBody" + } + } + ], + "tags": [ + "GatewayQRCodeGenerator" + ] + } + }, "/search/accounts": { "get": { "summary": "Search for accounts that match the conditions specified in the request.", @@ -17413,16 +17489,6 @@ } } }, - "EndDeviceQRCodeGeneratorParseBody": { - "type": "object", - "properties": { - "qr_code": { - "type": "string", - "format": "byte", - "description": "Raw QR code contents." - } - } - }, "EventAuthentication": { "type": "object", "properties": { @@ -23054,6 +23120,16 @@ } } }, + "v3EndDeviceQRCodeGeneratorParseBody": { + "type": "object", + "properties": { + "qr_code": { + "type": "string", + "format": "byte", + "description": "Raw QR code contents." + } + } + }, "v3EndDeviceRegistryCreateBody": { "type": "object", "properties": { @@ -24281,6 +24357,16 @@ }, "description": "GatewayDown contains downlink messages for the gateway." }, + "v3GatewayQRCodeGeneratorParseBody": { + "type": "object", + "properties": { + "qr_code": { + "type": "string", + "format": "byte", + "description": "Raw QR code contents." + } + } + }, "v3GatewayRadio": { "type": "object", "properties": { @@ -27953,6 +28039,37 @@ } } }, + "v3ParseGatewayQRCodeRequest": { + "type": "object", + "properties": { + "format_id": { + "type": "string", + "description": "QR code format identifier.\nIf this field is not specified, the server will default to ttigpro1." + }, + "qr_code": { + "type": "string", + "format": "byte", + "description": "Raw QR code contents." + } + } + }, + "v3ParseGatewayQRCodeResponse": { + "type": "object", + "properties": { + "format_id": { + "type": "string", + "description": "Identifier of the format used to parse the QR code data." + }, + "gateway_eui": { + "type": "string", + "format": "string", + "example": "70B3D57ED000ABCD" + }, + "owner_token": { + "type": "string" + } + } + }, "v3PayloadFormatter": { "type": "string", "enum": [ diff --git a/api/ttn/lorawan/v3/qrcodegenerator.proto b/api/ttn/lorawan/v3/qrcodegenerator.proto index 27bff44b03..820a858e25 100644 --- a/api/ttn/lorawan/v3/qrcodegenerator.proto +++ b/api/ttn/lorawan/v3/qrcodegenerator.proto @@ -20,6 +20,7 @@ import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "thethings/json/annotations.proto"; import "ttn/lorawan/v3/end_device.proto"; import "ttn/lorawan/v3/picture.proto"; import "validate/validate.proto"; @@ -130,3 +131,53 @@ service EndDeviceQRCodeGenerator { }; } } + +message ParseGatewayQRCodeRequest { + // QR code format identifier. + // If this field is not specified, the server will default to ttigpro1. + string format_id = 1 [(validate.rules).string = { + pattern: "^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$", + max_len: 36 + }]; + // Raw QR code contents. + bytes qr_code = 2 [(validate.rules).bytes = { + min_len: 10, + max_len: 1024 + }]; +} + +message ParseGatewayQRCodeResponse { + // Identifier of the format used to parse the QR code data. + string format_id = 1; + bytes gateway_eui = 2 [ + (validate.rules).bytes = { + len: 8, + ignore_empty: true + }, + (thethings.json.field) = { + marshaler_func: "go.thethings.network/lorawan-stack/v3/pkg/types.MarshalHEXBytes", + unmarshaler_func: "go.thethings.network/lorawan-stack/v3/pkg/types.Unmarshal8Bytes" + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + type: STRING, + format: "string", + example: "\"70B3D57ED000ABCD\"" + } + ]; + string owner_token = 3; +} + +// The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways. +service GatewayQRCodeGenerator { + // Parse QR Codes of known formats and return the information contained within. + rpc Parse(ParseGatewayQRCodeRequest) returns (ParseGatewayQRCodeResponse) { + option (google.api.http) = { + post: "/qr-codes/gateways/parse", + body: "*" + additional_bindings { + post: "/qr-codes/gateways/{format_id}/parse" + body: "*" + } + }; + } +} diff --git a/config/messages.json b/config/messages.json index b766109e77..d7461ca92f 100644 --- a/config/messages.json +++ b/config/messages.json @@ -9026,6 +9026,33 @@ "file": "enddevices.go" } }, + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_format": { + "translations": { + "en": "invalid format" + }, + "description": { + "package": "pkg/qrcodegenerator/qrcode/gateways", + "file": "gateways.go" + } + }, + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_length": { + "translations": { + "en": "invalid length" + }, + "description": { + "package": "pkg/qrcodegenerator/qrcode/gateways", + "file": "gateways.go" + } + }, + "error:pkg/qrcodegenerator/qrcode/gateways:unknown_format": { + "translations": { + "en": "format unknown" + }, + "description": { + "package": "pkg/qrcodegenerator/qrcode/gateways", + "file": "gateways.go" + } + }, "error:pkg/qrcodegenerator:format_not_found": { "translations": { "en": "format `{id}` not found" diff --git a/pkg/deviceclaimingserver/grpc_gateways_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go index baf104822a..2abf729324 100644 --- a/pkg/deviceclaimingserver/grpc_gateways_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -48,7 +48,7 @@ var ( authorizedCallOpt = grpc.PerRPCCredentials(authorizedMD) ) -func TestGatewayClaimingServer(t *testing.T) { // nolint:paralleltest +func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) ctx, cancelCtx := context.WithCancel(ctx) diff --git a/pkg/qrcodegenerator/grpc_gateways.go b/pkg/qrcodegenerator/grpc_gateways.go new file mode 100644 index 0000000000..4b61de1ea1 --- /dev/null +++ b/pkg/qrcodegenerator/grpc_gateways.go @@ -0,0 +1,66 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrcodegenerator + +import ( + "context" + + "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" +) + +type gatewayQRCodeGeneratorServer struct { + ttnpb.UnimplementedGatewayQRCodeGeneratorServer + + QRG *QRCodeGenerator +} + +// GetFormat implements EndDeviceQRCodeGenerator. +func (s *gatewayQRCodeGeneratorServer) GetFormat( + ctx context.Context, + req *ttnpb.GetQRCodeFormatRequest, +) (*ttnpb.QRCodeFormat, error) { + _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) + if err != nil { + return nil, err + } + format := s.QRG.gateways.GetGatewayFormat(req.FormatId) + if format == nil { + return nil, errFormatNotFound.New() + } + return format.Format(), nil +} + +// Parse implements EndDeviceQRCodeGenerator. +func (s *gatewayQRCodeGeneratorServer) Parse( + ctx context.Context, + req *ttnpb.ParseGatewayQRCodeRequest, +) (*ttnpb.ParseGatewayQRCodeResponse, error) { + _, err := rpcmetadata.WithForwardedAuth(ctx, s.QRG.AllowInsecureForCredentials()) + if err != nil { + return nil, err + } + + data, err := s.QRG.gateways.Parse(req.FormatId, req.QrCode) + if err != nil { + return nil, err + } + + return &ttnpb.ParseGatewayQRCodeResponse{ + FormatId: data.FormatID(), + GatewayEui: data.GatewayEUI().Bytes(), + OwnerToken: data.OwnerToken(), + }, nil +} diff --git a/pkg/qrcodegenerator/grpc_gateways_test.go b/pkg/qrcodegenerator/grpc_gateways_test.go new file mode 100644 index 0000000000..0c6971fdad --- /dev/null +++ b/pkg/qrcodegenerator/grpc_gateways_test.go @@ -0,0 +1,140 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package qrcodegenerator_test + +import ( + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/component" + componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestGatewayQRCodeParsing(t *testing.T) { + t.Parallel() + + a := assertions.New(t) + ctx := log.NewContext(test.Context(), test.GetLogger(t)) + + c := componenttest.NewComponent(t, &component.Config{}) + ttigpro1 := new(gateways.TTIGPRO1Format) + qrg, err := qrcodegenerator.New(c, + &qrcodegenerator.Config{}, + qrcodegenerator.WithGatewayFormat(ttigpro1.ID(), ttigpro1), + ) + test.Must(qrg, err) + + componenttest.StartComponent(t, c) + t.Cleanup(func() { c.Close() }) + + mustHavePeer(ctx, c, ttnpb.ClusterRole_QR_CODE_GENERATOR) + + client := ttnpb.NewGatewayQRCodeGeneratorClient(c.LoopbackConn()) + + for _, tc := range []struct { + Name string + FormatID string + GetQRData func() []byte + Assertion func(*assertions.Assertion, *ttnpb.ParseGatewayQRCodeResponse, error) bool + }{ + { + Name: "EmptyData", + GetQRData: func() []byte { + return []byte{} + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "UnknownFormat", + FormatID: "unknown", + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "InvalidFormat", + FormatID: "tr005", + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.BeNil) { + return false + } + if !a.So(errors.IsInvalidArgument(err), should.BeTrue) { + return false + } + return true + }, + }, + { + Name: "ValidTTIGPRO1", + FormatID: ttigpro1.ID(), + GetQRData: func() []byte { + return []byte(`https://ttig.pro/c/ec656efffe000128/abcdef123456`) + }, + Assertion: func(a *assertions.Assertion, resp *ttnpb.ParseGatewayQRCodeResponse, err error) bool { + if !a.So(resp, should.NotBeNil) { + return false + } + if !a.So(err, should.BeNil) { + return false + } + a.So(resp.FormatId, should.Equal, ttigpro1.ID()) + a.So(resp.GatewayEui, should.Resemble, []uint8{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}) + a.So(resp.OwnerToken, should.Equal, "abcdef123456") + + return true + }, + }, + } { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + resp, err := client.Parse(ctx, &ttnpb.ParseGatewayQRCodeRequest{ + FormatId: tc.FormatID, + QrCode: tc.GetQRData(), + }, c.WithClusterAuth()) + if !a.So(tc.Assertion(a, resp, err), should.BeTrue) { + t.FailNow() + } + }) + } +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways.go b/pkg/qrcodegenerator/qrcode/gateways/gateways.go new file mode 100644 index 0000000000..06570c6ef4 --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways.go @@ -0,0 +1,127 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gateways provides a QR code parser for gateways. +package gateways + +import ( + "encoding" + + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" +) + +var ( + errUnknownFormat = errors.DefineInvalidArgument("unknown_format", "format unknown") + errInvalidLength = errors.DefineInvalidArgument("invalid_length", "invalid length") + errInvalidFormat = errors.DefineInvalidArgument("invalid_format", "invalid format") +) + +// Format is a gateway QR code format. +type Format interface { + Format() *ttnpb.QRCodeFormat + New() Data +} + +// Data represents gateway QR code data. +type Data interface { + // FormatID returns the ID of the format used to parse the QR Code data. + FormatID() string + GatewayEUI() types.EUI64 + OwnerToken() string + encoding.TextUnmarshaler +} + +type gatewayFormat struct { + id string + format Format +} + +// Server provides methods for gateways QR codes. +type Server struct { + gatewayFormats []gatewayFormat +} + +// New returns a new Server. +func New() *Server { + s := &Server{ + // Newer formats should be added to this slice first to + // preferentially match with those first. + gatewayFormats: []gatewayFormat{ + { + id: ttigpro1FormatID, + format: new(TTIGPRO1Format), + }, + }, + } + return s +} + +// RegisterGatewayFormat registers the given gateway QR code format. +func (s *Server) RegisterGatewayFormat(id string, f Format) { + s.gatewayFormats = append(s.gatewayFormats, gatewayFormat{ + id: id, + format: f, + }) +} + +// GetGatewayFormats returns the registered gateway QR code formats. +func (s *Server) GetGatewayFormats() map[string]Format { + ret := make(map[string]Format) + for _, gtwFormat := range s.gatewayFormats { + ret[gtwFormat.id] = gtwFormat.format + } + return ret +} + +// GetGatewayFormat returns the format by ID. +func (s *Server) GetGatewayFormat(id string) Format { + for _, gtwFormat := range s.gatewayFormats { + if gtwFormat.id == id { + return gtwFormat.format + } + } + return nil +} + +// Formats returns the registered gateway QR code formats. +func (s *Server) Formats() []*ttnpb.QRCodeFormat { + formats := make([]*ttnpb.QRCodeFormat, 0, len(s.gatewayFormats)) + for _, gtwFormat := range s.gatewayFormats { + formats = append(formats, gtwFormat.format.Format()) + } + return formats +} + +// Parse the given QR code data. If formatID is provided, only that format is used. +// Otherwise, the first format registered will be used. +func (s *Server) Parse(formatID string, data []byte) (ret Data, err error) { + for _, gtwFormat := range s.gatewayFormats { + // If format ID is provided, use only that. Otherwise, + // default to the first format listed in gatewayFormats. + if formatID != "" && formatID != gtwFormat.id { + continue + } + + f := gtwFormat.format.New() + if err := f.UnmarshalText(data); err != nil { + return nil, err + } + + return f, nil + } + + return nil, errUnknownFormat +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go new file mode 100644 index 0000000000..17d1439b4e --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/gateways_test.go @@ -0,0 +1,60 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways_test + +import ( + "strconv" + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestParseGatewaysAuthenticationCodes(t *testing.T) { + t.Parallel() + + for i, tc := range []struct { + FormatID string + Data []byte + ExpectedEUI types.EUI64 + ExpectedOwnerToken string + }{ + { + FormatID: "ttigpro1", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123456"), + ExpectedEUI: types.EUI64{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}, + ExpectedOwnerToken: "abcdef123456", + }, + } { + tc := tc + + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + qrCode := gateways.New() + + d, err := qrCode.Parse(tc.FormatID, tc.Data) + data := test.Must(d, err) + + a.So(data.FormatID(), should.Equal, tc.FormatID) + a.So(data.GatewayEUI(), should.Resemble, tc.ExpectedEUI) + a.So(data.OwnerToken(), should.Equal, tc.ExpectedOwnerToken) + }) + } +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go new file mode 100644 index 0000000000..96f5e1dccb --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1.go @@ -0,0 +1,95 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways + +import ( + "regexp" + + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" +) + +const ( + ttigpro1FormatID = "ttigpro1" +) + +// ttigpro1Regex is the regular expression to match the TTIGPRO1 format. +// The format is as follows: https://ttig.pro/c/{16 lowercase base16 chars}/{12 base62 chars}. +var ttigpro1Regex = regexp.MustCompile(`^https://ttig\.pro/c/([a-f0-9]{16})/([a-z0-9]{12})$`) + +// TTIGPRO1 is a format for gateway identification QR codes. +type ttigpro1 struct { + gatewayEUI types.EUI64 + ownerToken string +} + +// UnmarshalText implements the TextUnmarshaler interface. +func (m *ttigpro1) UnmarshalText(text []byte) error { + // Match the URL against the pattern + matches := ttigpro1Regex.FindStringSubmatch(string(text)) + if matches == nil || len(matches) != 3 { + return errInvalidFormat + } + + if err := m.gatewayEUI.UnmarshalText([]byte(matches[1])); err != nil { + return err + } + + m.ownerToken = matches[2] + + if len(m.ownerToken) != 12 /* owner token length */ { + return errInvalidLength + } + + return nil +} + +// FormatID implements the Data interface. +func (*ttigpro1) FormatID() string { + return ttigpro1FormatID +} + +func (m *ttigpro1) GatewayEUI() types.EUI64 { + return m.gatewayEUI +} + +func (m *ttigpro1) OwnerToken() string { + return m.ownerToken +} + +// TTIGPRO1Format implements the TTIGPRO1 Format. +type TTIGPRO1Format struct{} + +// Format implements the Format interface. +func (TTIGPRO1Format) Format() *ttnpb.QRCodeFormat { + return &ttnpb.QRCodeFormat{ + Name: "TTIGPRO1", + Description: "QR code format for The Things Indoor Gateway Pro.", + FieldMask: ttnpb.FieldMask( + "ids.eui", + "claim_authentication_code.secret.value", + ), + } +} + +// ID is the identifier of the format as a string. +func (TTIGPRO1Format) ID() string { + return ttigpro1FormatID +} + +// New implements the Format interface. +func (TTIGPRO1Format) New() Data { + return new(ttigpro1) +} diff --git a/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go new file mode 100644 index 0000000000..2d404a7495 --- /dev/null +++ b/pkg/qrcodegenerator/qrcode/gateways/ttigpro1_test.go @@ -0,0 +1,113 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gateways + +import ( + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestTTIGPRO1(t *testing.T) { + t.Parallel() + + t.Run("Decode", func(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + Name string + Data []byte + Expected ttigpro1 + ErrorAssertion func(t *testing.T, err error) bool + }{ + { + Name: "CorrectQRCode", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123456"), + Expected: ttigpro1{ + gatewayEUI: types.EUI64{0xec, 0x65, 0x6e, 0xff, 0xfe, 0x00, 0x01, 0x28}, + ownerToken: "abcdef123456", + }, + }, + { + Name: "InvalidURLPrefix", + Data: []byte("https://example.com/c/ec656efffe000128/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/EUINotLowercase", + Data: []byte("https://ttig.pro/c/EC656effFe000128/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/EUILength", + Data: []byte("https://ttig.pro/c/ec656efffe00012/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/EUINotBase16", + Data: []byte("https://ttig.pro/c/ec656efffe00012g/abcdef12"), + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/OwnerTokenLength", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef123"), + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + { + Name: "Invalid/OwnerTokenNotBase62", + Data: []byte("https://ttig.pro/c/ec656efffe000128/abcdef12!"), + ErrorAssertion: func(t *testing.T, err error) bool { + t.Helper() + return assertions.New(t).So(errors.IsInvalidArgument(err), should.BeTrue) + }, + }, + } { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + a := assertions.New(t) + + var data ttigpro1 + err := data.UnmarshalText(tc.Data) + if tc.ErrorAssertion != nil { + a.So(tc.ErrorAssertion(t, err), should.BeTrue) + return + } + if !a.So(err, should.BeNil) || !a.So(data, should.Resemble, tc.Expected) { + t.FailNow() + } + }) + } + }) +} diff --git a/pkg/qrcodegenerator/qrcodegenerator.go b/pkg/qrcodegenerator/qrcodegenerator.go index 6cd66e13d8..334a7f6d61 100644 --- a/pkg/qrcodegenerator/qrcodegenerator.go +++ b/pkg/qrcodegenerator/qrcodegenerator.go @@ -23,6 +23,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/enddevices" + "go.thethings.network/lorawan-stack/v3/pkg/qrcodegenerator/qrcode/gateways" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "google.golang.org/grpc" ) @@ -35,23 +36,28 @@ type QRCodeGenerator struct { ctx context.Context endDevices *enddevices.Server + gateways *gateways.Server grpc struct { endDeviceQRCodeGenerator *endDeviceQRCodeGeneratorServer + gatewayQRCodeGenerator *gatewayQRCodeGeneratorServer } } var errFormatNotFound = errors.DefineNotFound("format_not_found", "format `{id}` not found") // New returns a new *QRCodeGenerator. -func New(c *component.Component, conf *Config, opts ...Option) (*QRCodeGenerator, error) { +func New(c *component.Component, _ *Config, opts ...Option) (*QRCodeGenerator, error) { ctx := log.NewContextWithField(c.Context(), "namespace", "qrcodegenerator") qrg := &QRCodeGenerator{ Component: c, ctx: ctx, } qrg.grpc.endDeviceQRCodeGenerator = &endDeviceQRCodeGeneratorServer{QRG: qrg} + qrg.grpc.gatewayQRCodeGenerator = &gatewayQRCodeGeneratorServer{QRG: qrg} + qrg.endDevices = enddevices.New(ctx) + qrg.gateways = gateways.New() c.RegisterGRPC(qrg) @@ -72,22 +78,31 @@ func WithEndDeviceFormat(id string, f enddevices.Format) Option { } } +// WithGatewayFormat configures QRCodeGenerator with a GatewayFormat. +func WithGatewayFormat(id string, f gateways.Format) Option { + return func(qrg *QRCodeGenerator) { + qrg.gateways.RegisterGatewayFormat(id, f) + } +} + // Context returns the context of the QR Code Generator. func (qrg *QRCodeGenerator) Context() context.Context { return qrg.ctx } // Roles returns the roles that the QR Code Generator fulfills. -func (qrg *QRCodeGenerator) Roles() []ttnpb.ClusterRole { +func (*QRCodeGenerator) Roles() []ttnpb.ClusterRole { return []ttnpb.ClusterRole{ttnpb.ClusterRole_QR_CODE_GENERATOR} } // RegisterServices registers services provided by qrg at s. func (qrg *QRCodeGenerator) RegisterServices(s *grpc.Server) { ttnpb.RegisterEndDeviceQRCodeGeneratorServer(s, qrg.grpc.endDeviceQRCodeGenerator) + ttnpb.RegisterGatewayQRCodeGeneratorServer(s, qrg.grpc.gatewayQRCodeGenerator) } // RegisterHandlers registers gRPC handlers. func (qrg *QRCodeGenerator) RegisterHandlers(s *runtime.ServeMux, conn *grpc.ClientConn) { - ttnpb.RegisterEndDeviceQRCodeGeneratorHandler(qrg.Context(), s, conn) + ttnpb.RegisterEndDeviceQRCodeGeneratorHandler(qrg.Context(), s, conn) //nolint:errcheck + ttnpb.RegisterGatewayQRCodeGeneratorHandler(qrg.Context(), s, conn) //nolint:errcheck } diff --git a/pkg/ttnpb/qrcodegenerator.pb.go b/pkg/ttnpb/qrcodegenerator.pb.go index 4897d4334b..ac6ea52b6d 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.go +++ b/pkg/ttnpb/qrcodegenerator.pb.go @@ -21,6 +21,7 @@ package ttnpb import ( + _ "github.com/TheThingsIndustries/protoc-gen-go-json/annotations" _ "github.com/envoyproxy/protoc-gen-validate/validate" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" _ "google.golang.org/genproto/googleapis/api/annotations" @@ -437,6 +438,128 @@ func (x *ParseEndDeviceQRCodeResponse) GetEndDeviceTemplate() *EndDeviceTemplate return nil } +type ParseGatewayQRCodeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // QR code format identifier. + // If this field is not specified, the server will default to ttigpro1. + FormatId string `protobuf:"bytes,1,opt,name=format_id,json=formatId,proto3" json:"format_id,omitempty"` + // Raw QR code contents. + QrCode []byte `protobuf:"bytes,2,opt,name=qr_code,json=qrCode,proto3" json:"qr_code,omitempty"` +} + +func (x *ParseGatewayQRCodeRequest) Reset() { + *x = ParseGatewayQRCodeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ParseGatewayQRCodeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseGatewayQRCodeRequest) ProtoMessage() {} + +func (x *ParseGatewayQRCodeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseGatewayQRCodeRequest.ProtoReflect.Descriptor instead. +func (*ParseGatewayQRCodeRequest) Descriptor() ([]byte, []int) { + return file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescGZIP(), []int{7} +} + +func (x *ParseGatewayQRCodeRequest) GetFormatId() string { + if x != nil { + return x.FormatId + } + return "" +} + +func (x *ParseGatewayQRCodeRequest) GetQrCode() []byte { + if x != nil { + return x.QrCode + } + return nil +} + +type ParseGatewayQRCodeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Identifier of the format used to parse the QR code data. + FormatId string `protobuf:"bytes,1,opt,name=format_id,json=formatId,proto3" json:"format_id,omitempty"` + GatewayEui []byte `protobuf:"bytes,2,opt,name=gateway_eui,json=gatewayEui,proto3" json:"gateway_eui,omitempty"` + OwnerToken string `protobuf:"bytes,3,opt,name=owner_token,json=ownerToken,proto3" json:"owner_token,omitempty"` +} + +func (x *ParseGatewayQRCodeResponse) Reset() { + *x = ParseGatewayQRCodeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ParseGatewayQRCodeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseGatewayQRCodeResponse) ProtoMessage() {} + +func (x *ParseGatewayQRCodeResponse) ProtoReflect() protoreflect.Message { + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseGatewayQRCodeResponse.ProtoReflect.Descriptor instead. +func (*ParseGatewayQRCodeResponse) Descriptor() ([]byte, []int) { + return file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescGZIP(), []int{8} +} + +func (x *ParseGatewayQRCodeResponse) GetFormatId() string { + if x != nil { + return x.FormatId + } + return "" +} + +func (x *ParseGatewayQRCodeResponse) GetGatewayEui() []byte { + if x != nil { + return x.GatewayEui + } + return nil +} + +func (x *ParseGatewayQRCodeResponse) GetOwnerToken() string { + if x != nil { + return x.OwnerToken + } + return "" +} + type GenerateEndDeviceQRCodeRequest_Image struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -449,7 +572,7 @@ type GenerateEndDeviceQRCodeRequest_Image struct { func (x *GenerateEndDeviceQRCodeRequest_Image) Reset() { *x = GenerateEndDeviceQRCodeRequest_Image{} if protoimpl.UnsafeEnabled { - mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -462,7 +585,7 @@ func (x *GenerateEndDeviceQRCodeRequest_Image) String() string { func (*GenerateEndDeviceQRCodeRequest_Image) ProtoMessage() {} func (x *GenerateEndDeviceQRCodeRequest_Image) ProtoReflect() protoreflect.Message { - mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8] + mi := &file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -500,124 +623,167 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_rawDesc = []byte{ 0x6f, 0x74, 0x6f, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x76, 0x32, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, - 0x2f, 0x76, 0x33, 0x2f, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, 0x0a, 0x0c, - 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, - 0x02, 0x18, 0x64, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xc8, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, - 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, - 0x22, 0xdd, 0x01, 0x0a, 0x0d, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x73, 0x12, 0x72, 0x0a, 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x73, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, - 0x2c, 0xfa, 0x42, 0x29, 0x9a, 0x01, 0x26, 0x22, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, - 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, - 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x07, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x1a, 0x58, 0x0a, 0x0c, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0x5e, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, - 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, - 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, - 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, - 0x22, 0xaa, 0x02, 0x0a, 0x1e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, + 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2f, 0x6a, + 0x73, 0x6f, 0x6e, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, + 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x74, 0x74, 0x6e, 0x2f, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x01, + 0x0a, 0x0c, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x1b, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, + 0x04, 0x72, 0x02, 0x18, 0x64, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x18, 0xc8, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, + 0x73, 0x6b, 0x22, 0xdd, 0x01, 0x0a, 0x0d, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x73, 0x12, 0x72, 0x0a, 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x73, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x42, 0x2c, 0xfa, 0x42, 0x29, 0x9a, 0x01, 0x26, 0x22, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, - 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x65, 0x6e, 0x64, - 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, - 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, - 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, - 0x10, 0x01, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, - 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, - 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, - 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, - 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x1a, 0x32, 0x0a, 0x05, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x12, 0x29, 0x0a, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x2a, 0x05, 0x18, 0xe8, 0x07, - 0x28, 0x0a, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x5b, 0x0a, - 0x16, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x74, 0x6e, - 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x69, 0x63, 0x74, - 0x75, 0x72, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x1b, 0x50, + 0x07, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x1a, 0x58, 0x0a, 0x0c, 0x46, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x32, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, + 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x5e, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, + 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x49, 0x64, 0x22, 0xaa, 0x02, 0x0a, 0x1e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xfa, 0x42, 0x24, 0x72, 0x22, 0x18, + 0x24, 0x32, 0x1e, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, + 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, + 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0a, 0x65, + 0x6e, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, + 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, + 0x01, 0x02, 0x10, 0x01, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x4a, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, + 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x1a, 0x32, 0x0a, 0x05, 0x49, + 0x6d, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x0a, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x2a, 0x05, 0x18, + 0xe8, 0x07, 0x28, 0x0a, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, + 0x5b, 0x0a, 0x16, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x2d, 0x0a, + 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x69, + 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x8b, 0x01, 0x0a, + 0x1b, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, + 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x2a, 0xfa, 0x42, 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, + 0x2d, 0x39, 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, + 0x39, 0x5d, 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, + 0x80, 0x08, 0x52, 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, - 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, - 0x42, 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, - 0x5d, 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, - 0x29, 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, - 0x52, 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x1c, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x13, 0x65, 0x6e, 0x64, 0x5f, 0x64, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, - 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x11, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x32, 0xfe, 0x04, 0x0a, 0x18, 0x45, 0x6e, - 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x47, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x84, 0x01, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x46, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x12, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x74, - 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, - 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x31, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x2b, 0x12, 0x29, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, - 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x12, 0x6b, 0x0a, - 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, - 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x73, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, 0x1d, 0x2f, 0x71, 0x72, - 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x84, 0x01, 0x0a, 0x08, 0x47, - 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, - 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x71, 0x72, 0x2d, + 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x49, 0x64, 0x12, 0x51, 0x0a, 0x13, 0x65, 0x6e, 0x64, 0x5f, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, + 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x11, 0x65, 0x6e, 0x64, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x22, 0x89, 0x01, 0x0a, 0x19, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, + 0x27, 0x72, 0x25, 0x18, 0x24, 0x32, 0x21, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, + 0x28, 0x3f, 0x3a, 0x5b, 0x2d, 0x5d, 0x3f, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, + 0x7b, 0x32, 0x2c, 0x7d, 0x24, 0x7c, 0x5e, 0x24, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x49, 0x64, 0x12, 0x23, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x7a, 0x05, 0x10, 0x0a, 0x18, 0x80, 0x08, 0x52, + 0x06, 0x71, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xb3, 0x02, 0x0a, 0x1a, 0x50, 0x61, 0x72, 0x73, + 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x49, 0x64, 0x12, 0xd6, 0x01, 0x0a, 0x0b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x5f, + 0x65, 0x75, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x42, 0xb4, 0x01, 0x92, 0x41, 0x21, 0x4a, + 0x12, 0x22, 0x37, 0x30, 0x42, 0x33, 0x44, 0x35, 0x37, 0x45, 0x44, 0x30, 0x30, 0x30, 0x41, 0x42, + 0x43, 0x44, 0x22, 0x9a, 0x02, 0x01, 0x07, 0xa2, 0x02, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0xfa, 0x42, 0x06, 0x7a, 0x04, 0x68, 0x08, 0x70, 0x01, 0xea, 0xaa, 0x19, 0x82, 0x01, 0x0a, 0x3f, + 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, + 0x4d, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6c, 0x48, 0x45, 0x58, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, + 0x3f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2e, 0x55, 0x6e, 0x6d, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6c, 0x38, 0x42, 0x79, 0x74, 0x65, 0x73, + 0x52, 0x0a, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x45, 0x75, 0x69, 0x12, 0x1f, 0x0a, 0x0b, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xfe, 0x04, + 0x0a, 0x18, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, + 0x65, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x84, 0x01, 0x0a, 0x09, 0x47, + 0x65, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x26, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, + 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x52, 0x43, + 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, + 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x31, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2b, 0x12, 0x29, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, + 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x74, 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, + 0x7d, 0x12, 0x6b, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, + 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, + 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, + 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x12, + 0x1d, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x73, 0x12, 0x84, + 0x01, 0x0a, 0x08, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, + 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x74, + 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x47, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, + 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0xb8, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, + 0x2b, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, + 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, + 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, + 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, + 0x72, 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x54, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x4e, 0x3a, 0x01, 0x2a, 0x5a, 0x2c, 0x3a, 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x12, 0xb8, 0x01, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2b, 0x2e, 0x74, 0x74, - 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, - 0x73, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, - 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, - 0x6e, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x54, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x4e, 0x3a, 0x01, - 0x2a, 0x5a, 0x2c, 0x3a, 0x01, 0x2a, 0x22, 0x27, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, - 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x7b, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x22, - 0x1b, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, 0x6e, 0x64, 0x2d, 0x64, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x2c, 0x92, 0x41, - 0x29, 0x12, 0x27, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, - 0x70, 0x61, 0x72, 0x73, 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x20, 0x51, 0x52, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, + 0x73, 0x2f, 0x7b, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, + 0x72, 0x73, 0x65, 0x22, 0x1b, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x65, + 0x6e, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x1a, 0x2c, 0x92, 0x41, 0x29, 0x12, 0x27, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x70, 0x61, 0x72, 0x73, 0x65, 0x20, 0x65, 0x6e, 0x64, 0x20, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x20, 0x51, 0x52, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x32, 0xc9, + 0x01, 0x0a, 0x16, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, + 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0xae, 0x01, 0x0a, 0x05, 0x50, 0x61, + 0x72, 0x73, 0x65, 0x12, 0x29, 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, + 0x6e, 0x2e, 0x76, 0x33, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x51, 0x52, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, + 0x2e, 0x74, 0x74, 0x6e, 0x2e, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2e, 0x76, 0x33, 0x2e, + 0x50, 0x61, 0x72, 0x73, 0x65, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x51, 0x52, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4e, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x48, 0x3a, 0x01, 0x2a, 0x5a, 0x29, 0x3a, 0x01, 0x2a, 0x22, 0x24, 0x2f, 0x71, 0x72, 0x2d, + 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x73, 0x2f, 0x7b, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x5f, 0x69, 0x64, 0x7d, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x22, 0x18, 0x2f, 0x71, 0x72, 0x2d, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x2f, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x73, 0x2f, 0x70, 0x61, 0x72, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x6f, 0x2e, 0x74, 0x68, 0x65, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6f, 0x72, 0x61, 0x77, 0x61, 0x6e, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x74, 0x6e, 0x70, 0x62, 0x62, 0x06, 0x70, @@ -636,7 +802,7 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescGZIP() []byte { return file_ttn_lorawan_v3_qrcodegenerator_proto_rawDescData } -var file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes = []interface{}{ (*QRCodeFormat)(nil), // 0: ttn.lorawan.v3.QRCodeFormat (*QRCodeFormats)(nil), // 1: ttn.lorawan.v3.QRCodeFormats @@ -645,32 +811,36 @@ var file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes = []interface{}{ (*GenerateQRCodeResponse)(nil), // 4: ttn.lorawan.v3.GenerateQRCodeResponse (*ParseEndDeviceQRCodeRequest)(nil), // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeRequest (*ParseEndDeviceQRCodeResponse)(nil), // 6: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse - nil, // 7: ttn.lorawan.v3.QRCodeFormats.FormatsEntry - (*GenerateEndDeviceQRCodeRequest_Image)(nil), // 8: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image - (*fieldmaskpb.FieldMask)(nil), // 9: google.protobuf.FieldMask - (*EndDevice)(nil), // 10: ttn.lorawan.v3.EndDevice - (*Picture)(nil), // 11: ttn.lorawan.v3.Picture - (*EndDeviceTemplate)(nil), // 12: ttn.lorawan.v3.EndDeviceTemplate - (*emptypb.Empty)(nil), // 13: google.protobuf.Empty + (*ParseGatewayQRCodeRequest)(nil), // 7: ttn.lorawan.v3.ParseGatewayQRCodeRequest + (*ParseGatewayQRCodeResponse)(nil), // 8: ttn.lorawan.v3.ParseGatewayQRCodeResponse + nil, // 9: ttn.lorawan.v3.QRCodeFormats.FormatsEntry + (*GenerateEndDeviceQRCodeRequest_Image)(nil), // 10: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image + (*fieldmaskpb.FieldMask)(nil), // 11: google.protobuf.FieldMask + (*EndDevice)(nil), // 12: ttn.lorawan.v3.EndDevice + (*Picture)(nil), // 13: ttn.lorawan.v3.Picture + (*EndDeviceTemplate)(nil), // 14: ttn.lorawan.v3.EndDeviceTemplate + (*emptypb.Empty)(nil), // 15: google.protobuf.Empty } var file_ttn_lorawan_v3_qrcodegenerator_proto_depIdxs = []int32{ - 9, // 0: ttn.lorawan.v3.QRCodeFormat.field_mask:type_name -> google.protobuf.FieldMask - 7, // 1: ttn.lorawan.v3.QRCodeFormats.formats:type_name -> ttn.lorawan.v3.QRCodeFormats.FormatsEntry - 10, // 2: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice - 8, // 3: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.image:type_name -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image - 11, // 4: ttn.lorawan.v3.GenerateQRCodeResponse.image:type_name -> ttn.lorawan.v3.Picture - 12, // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse.end_device_template:type_name -> ttn.lorawan.v3.EndDeviceTemplate + 11, // 0: ttn.lorawan.v3.QRCodeFormat.field_mask:type_name -> google.protobuf.FieldMask + 9, // 1: ttn.lorawan.v3.QRCodeFormats.formats:type_name -> ttn.lorawan.v3.QRCodeFormats.FormatsEntry + 12, // 2: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.end_device:type_name -> ttn.lorawan.v3.EndDevice + 10, // 3: ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.image:type_name -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest.Image + 13, // 4: ttn.lorawan.v3.GenerateQRCodeResponse.image:type_name -> ttn.lorawan.v3.Picture + 14, // 5: ttn.lorawan.v3.ParseEndDeviceQRCodeResponse.end_device_template:type_name -> ttn.lorawan.v3.EndDeviceTemplate 0, // 6: ttn.lorawan.v3.QRCodeFormats.FormatsEntry.value:type_name -> ttn.lorawan.v3.QRCodeFormat 2, // 7: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:input_type -> ttn.lorawan.v3.GetQRCodeFormatRequest - 13, // 8: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:input_type -> google.protobuf.Empty + 15, // 8: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:input_type -> google.protobuf.Empty 3, // 9: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:input_type -> ttn.lorawan.v3.GenerateEndDeviceQRCodeRequest 5, // 10: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeRequest - 0, // 11: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:output_type -> ttn.lorawan.v3.QRCodeFormat - 1, // 12: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:output_type -> ttn.lorawan.v3.QRCodeFormats - 4, // 13: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:output_type -> ttn.lorawan.v3.GenerateQRCodeResponse - 6, // 14: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeResponse - 11, // [11:15] is the sub-list for method output_type - 7, // [7:11] is the sub-list for method input_type + 7, // 11: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:input_type -> ttn.lorawan.v3.ParseGatewayQRCodeRequest + 0, // 12: ttn.lorawan.v3.EndDeviceQRCodeGenerator.GetFormat:output_type -> ttn.lorawan.v3.QRCodeFormat + 1, // 13: ttn.lorawan.v3.EndDeviceQRCodeGenerator.ListFormats:output_type -> ttn.lorawan.v3.QRCodeFormats + 4, // 14: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Generate:output_type -> ttn.lorawan.v3.GenerateQRCodeResponse + 6, // 15: ttn.lorawan.v3.EndDeviceQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseEndDeviceQRCodeResponse + 8, // 16: ttn.lorawan.v3.GatewayQRCodeGenerator.Parse:output_type -> ttn.lorawan.v3.ParseGatewayQRCodeResponse + 12, // [12:17] is the sub-list for method output_type + 7, // [7:12] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name 7, // [7:7] is the sub-list for extension extendee 0, // [0:7] is the sub-list for field type_name @@ -768,7 +938,31 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { return nil } } + file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseGatewayQRCodeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ParseGatewayQRCodeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ttn_lorawan_v3_qrcodegenerator_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GenerateEndDeviceQRCodeRequest_Image); i { case 0: return &v.state @@ -787,9 +981,9 @@ func file_ttn_lorawan_v3_qrcodegenerator_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_ttn_lorawan_v3_qrcodegenerator_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_ttn_lorawan_v3_qrcodegenerator_proto_goTypes, DependencyIndexes: file_ttn_lorawan_v3_qrcodegenerator_proto_depIdxs, diff --git a/pkg/ttnpb/qrcodegenerator.pb.gw.go b/pkg/ttnpb/qrcodegenerator.pb.gw.go index 60faa7ba36..cffa22581f 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.gw.go +++ b/pkg/ttnpb/qrcodegenerator.pb.gw.go @@ -214,6 +214,92 @@ func local_request_EndDeviceQRCodeGenerator_Parse_1(ctx context.Context, marshal } +func request_GatewayQRCodeGenerator_Parse_0(ctx context.Context, marshaler runtime.Marshaler, client GatewayQRCodeGeneratorClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.Parse(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GatewayQRCodeGenerator_Parse_0(ctx context.Context, marshaler runtime.Marshaler, server GatewayQRCodeGeneratorServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.Parse(ctx, &protoReq) + return msg, metadata, err + +} + +func request_GatewayQRCodeGenerator_Parse_1(ctx context.Context, marshaler runtime.Marshaler, client GatewayQRCodeGeneratorClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["format_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "format_id") + } + + protoReq.FormatId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "format_id", err) + } + + msg, err := client.Parse(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_GatewayQRCodeGenerator_Parse_1(ctx context.Context, marshaler runtime.Marshaler, server GatewayQRCodeGeneratorServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ParseGatewayQRCodeRequest + var metadata runtime.ServerMetadata + + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["format_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "format_id") + } + + protoReq.FormatId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "format_id", err) + } + + msg, err := server.Parse(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterEndDeviceQRCodeGeneratorHandlerServer registers the http handlers for service EndDeviceQRCodeGenerator to "mux". // UnaryRPC :call EndDeviceQRCodeGeneratorServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -348,6 +434,65 @@ func RegisterEndDeviceQRCodeGeneratorHandlerServer(ctx context.Context, mux *run return nil } +// RegisterGatewayQRCodeGeneratorHandlerServer registers the http handlers for service GatewayQRCodeGenerator to "mux". +// UnaryRPC :call GatewayQRCodeGeneratorServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterGatewayQRCodeGeneratorHandlerFromEndpoint instead. +func RegisterGatewayQRCodeGeneratorHandlerServer(ctx context.Context, mux *runtime.ServeMux, server GatewayQRCodeGeneratorServer) error { + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GatewayQRCodeGenerator_Parse_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/{format_id}/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_GatewayQRCodeGenerator_Parse_1(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + // RegisterEndDeviceQRCodeGeneratorHandlerFromEndpoint is same as RegisterEndDeviceQRCodeGeneratorHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterEndDeviceQRCodeGeneratorHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -522,3 +667,100 @@ var ( forward_EndDeviceQRCodeGenerator_Parse_1 = runtime.ForwardResponseMessage ) + +// RegisterGatewayQRCodeGeneratorHandlerFromEndpoint is same as RegisterGatewayQRCodeGeneratorHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterGatewayQRCodeGeneratorHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterGatewayQRCodeGeneratorHandler(ctx, mux, conn) +} + +// RegisterGatewayQRCodeGeneratorHandler registers the http handlers for service GatewayQRCodeGenerator to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterGatewayQRCodeGeneratorHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterGatewayQRCodeGeneratorHandlerClient(ctx, mux, NewGatewayQRCodeGeneratorClient(conn)) +} + +// RegisterGatewayQRCodeGeneratorHandlerClient registers the http handlers for service GatewayQRCodeGenerator +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "GatewayQRCodeGeneratorClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GatewayQRCodeGeneratorClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "GatewayQRCodeGeneratorClient" to call the correct interceptors. +func RegisterGatewayQRCodeGeneratorHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GatewayQRCodeGeneratorClient) error { + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GatewayQRCodeGenerator_Parse_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_GatewayQRCodeGenerator_Parse_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse", runtime.WithHTTPPathPattern("/qr-codes/gateways/{format_id}/parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_GatewayQRCodeGenerator_Parse_1(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_GatewayQRCodeGenerator_Parse_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_GatewayQRCodeGenerator_Parse_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"qr-codes", "gateways", "parse"}, "")) + + pattern_GatewayQRCodeGenerator_Parse_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"qr-codes", "gateways", "format_id", "parse"}, "")) +) + +var ( + forward_GatewayQRCodeGenerator_Parse_0 = runtime.ForwardResponseMessage + + forward_GatewayQRCodeGenerator_Parse_1 = runtime.ForwardResponseMessage +) diff --git a/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go b/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go index 8f1eb3ddd5..0c6cc9e100 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go +++ b/pkg/ttnpb/qrcodegenerator.pb.paths.fm.go @@ -1579,6 +1579,26 @@ var ParseEndDeviceQRCodeResponseFieldPathsTopLevel = []string{ "end_device_template", "format_id", } +var ParseGatewayQRCodeRequestFieldPathsNested = []string{ + "format_id", + "qr_code", +} + +var ParseGatewayQRCodeRequestFieldPathsTopLevel = []string{ + "format_id", + "qr_code", +} +var ParseGatewayQRCodeResponseFieldPathsNested = []string{ + "format_id", + "gateway_eui", + "owner_token", +} + +var ParseGatewayQRCodeResponseFieldPathsTopLevel = []string{ + "format_id", + "gateway_eui", + "owner_token", +} var GenerateEndDeviceQRCodeRequest_ImageFieldPathsNested = []string{ "image_size", } diff --git a/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go b/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go index 8de3a0bcc7..1e7032e28e 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go +++ b/pkg/ttnpb/qrcodegenerator.pb.setters.fm.go @@ -278,6 +278,76 @@ func (dst *ParseEndDeviceQRCodeResponse) SetFields(src *ParseEndDeviceQRCodeResp return nil } +func (dst *ParseGatewayQRCodeRequest) SetFields(src *ParseGatewayQRCodeRequest, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "format_id": + if len(subs) > 0 { + return fmt.Errorf("'format_id' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FormatId = src.FormatId + } else { + var zero string + dst.FormatId = zero + } + case "qr_code": + if len(subs) > 0 { + return fmt.Errorf("'qr_code' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.QrCode = src.QrCode + } else { + dst.QrCode = nil + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + +func (dst *ParseGatewayQRCodeResponse) SetFields(src *ParseGatewayQRCodeResponse, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "format_id": + if len(subs) > 0 { + return fmt.Errorf("'format_id' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FormatId = src.FormatId + } else { + var zero string + dst.FormatId = zero + } + case "gateway_eui": + if len(subs) > 0 { + return fmt.Errorf("'gateway_eui' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.GatewayEui = src.GatewayEui + } else { + dst.GatewayEui = nil + } + case "owner_token": + if len(subs) > 0 { + return fmt.Errorf("'owner_token' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.OwnerToken = src.OwnerToken + } else { + var zero string + dst.OwnerToken = zero + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + func (dst *GenerateEndDeviceQRCodeRequest_Image) SetFields(src *GenerateEndDeviceQRCodeRequest_Image, paths ...string) error { for name, subs := range _processPaths(paths) { switch name { diff --git a/pkg/ttnpb/qrcodegenerator.pb.validate.go b/pkg/ttnpb/qrcodegenerator.pb.validate.go index 2d6bce669e..ef5173d639 100644 --- a/pkg/ttnpb/qrcodegenerator.pb.validate.go +++ b/pkg/ttnpb/qrcodegenerator.pb.validate.go @@ -783,6 +783,214 @@ var _ interface { ErrorName() string } = ParseEndDeviceQRCodeResponseValidationError{} +// ValidateFields checks the field values on ParseGatewayQRCodeRequest with the +// rules defined in the proto definition for this message. If any rules are +// violated, an error is returned. +func (m *ParseGatewayQRCodeRequest) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = ParseGatewayQRCodeRequestFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "format_id": + + if utf8.RuneCountInString(m.GetFormatId()) > 36 { + return ParseGatewayQRCodeRequestValidationError{ + field: "format_id", + reason: "value length must be at most 36 runes", + } + } + + if !_ParseGatewayQRCodeRequest_FormatId_Pattern.MatchString(m.GetFormatId()) { + return ParseGatewayQRCodeRequestValidationError{ + field: "format_id", + reason: "value does not match regex pattern \"^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$\"", + } + } + + case "qr_code": + + if l := len(m.GetQrCode()); l < 10 || l > 1024 { + return ParseGatewayQRCodeRequestValidationError{ + field: "qr_code", + reason: "value length must be between 10 and 1024 bytes, inclusive", + } + } + + default: + return ParseGatewayQRCodeRequestValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// ParseGatewayQRCodeRequestValidationError is the validation error returned by +// ParseGatewayQRCodeRequest.ValidateFields if the designated constraints +// aren't met. +type ParseGatewayQRCodeRequestValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ParseGatewayQRCodeRequestValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ParseGatewayQRCodeRequestValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ParseGatewayQRCodeRequestValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ParseGatewayQRCodeRequestValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ParseGatewayQRCodeRequestValidationError) ErrorName() string { + return "ParseGatewayQRCodeRequestValidationError" +} + +// Error satisfies the builtin error interface +func (e ParseGatewayQRCodeRequestValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sParseGatewayQRCodeRequest.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ParseGatewayQRCodeRequestValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ParseGatewayQRCodeRequestValidationError{} + +var _ParseGatewayQRCodeRequest_FormatId_Pattern = regexp.MustCompile("^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$") + +// ValidateFields checks the field values on ParseGatewayQRCodeResponse with +// the rules defined in the proto definition for this message. If any rules +// are violated, an error is returned. +func (m *ParseGatewayQRCodeResponse) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = ParseGatewayQRCodeResponseFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "format_id": + // no validation rules for FormatId + case "gateway_eui": + + if len(m.GetGatewayEui()) > 0 { + + if len(m.GetGatewayEui()) != 8 { + return ParseGatewayQRCodeResponseValidationError{ + field: "gateway_eui", + reason: "value length must be 8 bytes", + } + } + + } + + case "owner_token": + // no validation rules for OwnerToken + default: + return ParseGatewayQRCodeResponseValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// ParseGatewayQRCodeResponseValidationError is the validation error returned +// by ParseGatewayQRCodeResponse.ValidateFields if the designated constraints +// aren't met. +type ParseGatewayQRCodeResponseValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ParseGatewayQRCodeResponseValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ParseGatewayQRCodeResponseValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ParseGatewayQRCodeResponseValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ParseGatewayQRCodeResponseValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ParseGatewayQRCodeResponseValidationError) ErrorName() string { + return "ParseGatewayQRCodeResponseValidationError" +} + +// Error satisfies the builtin error interface +func (e ParseGatewayQRCodeResponseValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sParseGatewayQRCodeResponse.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ParseGatewayQRCodeResponseValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ParseGatewayQRCodeResponseValidationError{} + // ValidateFields checks the field values on // GenerateEndDeviceQRCodeRequest_Image with the rules defined in the proto // definition for this message. If any rules are violated, an error is returned. diff --git a/pkg/ttnpb/qrcodegenerator_grpc.pb.go b/pkg/ttnpb/qrcodegenerator_grpc.pb.go index b7c74458b5..2ef92b5fa0 100644 --- a/pkg/ttnpb/qrcodegenerator_grpc.pb.go +++ b/pkg/ttnpb/qrcodegenerator_grpc.pb.go @@ -242,3 +242,96 @@ var EndDeviceQRCodeGenerator_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "ttn/lorawan/v3/qrcodegenerator.proto", } + +const ( + GatewayQRCodeGenerator_Parse_FullMethodName = "/ttn.lorawan.v3.GatewayQRCodeGenerator/Parse" +) + +// GatewayQRCodeGeneratorClient is the client API for GatewayQRCodeGenerator service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GatewayQRCodeGeneratorClient interface { + // Parse QR Codes of known formats and return the information contained within. + Parse(ctx context.Context, in *ParseGatewayQRCodeRequest, opts ...grpc.CallOption) (*ParseGatewayQRCodeResponse, error) +} + +type gatewayQRCodeGeneratorClient struct { + cc grpc.ClientConnInterface +} + +func NewGatewayQRCodeGeneratorClient(cc grpc.ClientConnInterface) GatewayQRCodeGeneratorClient { + return &gatewayQRCodeGeneratorClient{cc} +} + +func (c *gatewayQRCodeGeneratorClient) Parse(ctx context.Context, in *ParseGatewayQRCodeRequest, opts ...grpc.CallOption) (*ParseGatewayQRCodeResponse, error) { + out := new(ParseGatewayQRCodeResponse) + err := c.cc.Invoke(ctx, GatewayQRCodeGenerator_Parse_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GatewayQRCodeGeneratorServer is the server API for GatewayQRCodeGenerator service. +// All implementations must embed UnimplementedGatewayQRCodeGeneratorServer +// for forward compatibility +type GatewayQRCodeGeneratorServer interface { + // Parse QR Codes of known formats and return the information contained within. + Parse(context.Context, *ParseGatewayQRCodeRequest) (*ParseGatewayQRCodeResponse, error) + mustEmbedUnimplementedGatewayQRCodeGeneratorServer() +} + +// UnimplementedGatewayQRCodeGeneratorServer must be embedded to have forward compatible implementations. +type UnimplementedGatewayQRCodeGeneratorServer struct { +} + +func (UnimplementedGatewayQRCodeGeneratorServer) Parse(context.Context, *ParseGatewayQRCodeRequest) (*ParseGatewayQRCodeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Parse not implemented") +} +func (UnimplementedGatewayQRCodeGeneratorServer) mustEmbedUnimplementedGatewayQRCodeGeneratorServer() { +} + +// UnsafeGatewayQRCodeGeneratorServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GatewayQRCodeGeneratorServer will +// result in compilation errors. +type UnsafeGatewayQRCodeGeneratorServer interface { + mustEmbedUnimplementedGatewayQRCodeGeneratorServer() +} + +func RegisterGatewayQRCodeGeneratorServer(s grpc.ServiceRegistrar, srv GatewayQRCodeGeneratorServer) { + s.RegisterService(&GatewayQRCodeGenerator_ServiceDesc, srv) +} + +func _GatewayQRCodeGenerator_Parse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ParseGatewayQRCodeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayQRCodeGeneratorServer).Parse(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayQRCodeGenerator_Parse_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayQRCodeGeneratorServer).Parse(ctx, req.(*ParseGatewayQRCodeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GatewayQRCodeGenerator_ServiceDesc is the grpc.ServiceDesc for GatewayQRCodeGenerator service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GatewayQRCodeGenerator_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ttn.lorawan.v3.GatewayQRCodeGenerator", + HandlerType: (*GatewayQRCodeGeneratorServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Parse", + Handler: _GatewayQRCodeGenerator_Parse_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ttn/lorawan/v3/qrcodegenerator.proto", +} diff --git a/pkg/ttnpb/qrcodegenerator_json.pb.go b/pkg/ttnpb/qrcodegenerator_json.pb.go index 5dc86c424a..56dd3ecb7c 100644 --- a/pkg/ttnpb/qrcodegenerator_json.pb.go +++ b/pkg/ttnpb/qrcodegenerator_json.pb.go @@ -9,6 +9,7 @@ package ttnpb import ( golang "github.com/TheThingsIndustries/protoc-gen-go-json/golang" jsonplugin "github.com/TheThingsIndustries/protoc-gen-go-json/jsonplugin" + types "go.thethings.network/lorawan-stack/v3/pkg/types" ) // MarshalProtoJSON marshals the QRCodeFormat message to JSON. @@ -204,3 +205,61 @@ func (x *ParseEndDeviceQRCodeResponse) UnmarshalProtoJSON(s *jsonplugin.Unmarsha func (x *ParseEndDeviceQRCodeResponse) UnmarshalJSON(b []byte) error { return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) } + +// MarshalProtoJSON marshals the ParseGatewayQRCodeResponse message to JSON. +func (x *ParseGatewayQRCodeResponse) MarshalProtoJSON(s *jsonplugin.MarshalState) { + if x == nil { + s.WriteNil() + return + } + s.WriteObjectStart() + var wroteField bool + if x.FormatId != "" || s.HasField("format_id") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("format_id") + s.WriteString(x.FormatId) + } + if len(x.GatewayEui) > 0 || s.HasField("gateway_eui") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("gateway_eui") + types.MarshalHEXBytes(s.WithField("gateway_eui"), x.GatewayEui) + } + if x.OwnerToken != "" || s.HasField("owner_token") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("owner_token") + s.WriteString(x.OwnerToken) + } + s.WriteObjectEnd() +} + +// MarshalJSON marshals the ParseGatewayQRCodeResponse to JSON. +func (x *ParseGatewayQRCodeResponse) MarshalJSON() ([]byte, error) { + return jsonplugin.DefaultMarshalerConfig.Marshal(x) +} + +// UnmarshalProtoJSON unmarshals the ParseGatewayQRCodeResponse message from JSON. +func (x *ParseGatewayQRCodeResponse) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { + if s.ReadNil() { + return + } + s.ReadObject(func(key string) { + switch key { + default: + s.ReadAny() // ignore unknown field + case "format_id", "formatId": + s.AddField("format_id") + x.FormatId = s.ReadString() + case "gateway_eui", "gatewayEui": + s.AddField("gateway_eui") + x.GatewayEui = types.Unmarshal8Bytes(s.WithField("gateway_eui", false)) + case "owner_token", "ownerToken": + s.AddField("owner_token") + x.OwnerToken = s.ReadString() + } + }) +} + +// UnmarshalJSON unmarshals the ParseGatewayQRCodeResponse from JSON. +func (x *ParseGatewayQRCodeResponse) UnmarshalJSON(b []byte) error { + return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) +} diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 8e18e357fe..9bea83f15d 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -2792,6 +2792,9 @@ "error:pkg/qrcodegenerator/qrcode/enddevices:no_dev_eui": "DevEUIがありません", "error:pkg/qrcodegenerator/qrcode/enddevices:no_join_eui": "JoinEUIがありません", "error:pkg/qrcodegenerator/qrcode/enddevices:unknown_format": "形式が不明", + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_format": "", + "error:pkg/qrcodegenerator/qrcode/gateways:invalid_length": "", + "error:pkg/qrcodegenerator/qrcode/gateways:unknown_format": "", "error:pkg/qrcodegenerator:format_not_found": "フォーマット `{id}` が見つかりません", "error:pkg/qrcodegenerator:unauthenticated": "呼び出しが認証されていません", "error:pkg/ratelimit:invalid_rate": "プロファイル `{name}` の無効なレート `{rate}`", diff --git a/sdk/js/generated/api-definition.json b/sdk/js/generated/api-definition.json index e554911d9f..bffb562105 100644 --- a/sdk/js/generated/api-definition.json +++ b/sdk/js/generated/api-definition.json @@ -7147,6 +7147,27 @@ ] } }, + "GatewayQRCodeGenerator": { + "Parse": { + "file": "ttn/lorawan/v3/qrcodegenerator.proto", + "http": [ + { + "method": "post", + "pattern": "/qr-codes/gateways/parse", + "body": "*", + "parameters": [] + }, + { + "method": "post", + "pattern": "/qr-codes/gateways/{format_id}/parse", + "body": "*", + "parameters": [ + "format_id" + ] + } + ] + } + }, "EndDeviceRegistrySearch": { "SearchEndDevices": { "file": "ttn/lorawan/v3/search_services.proto", diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index 7aa60fc4e2..f85d137f0e 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -48254,6 +48254,122 @@ } ] }, + { + "name": "ParseGatewayQRCodeRequest", + "longName": "ParseGatewayQRCodeRequest", + "fullName": "ttn.lorawan.v3.ParseGatewayQRCodeRequest", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "format_id", + "description": "QR code format identifier.\nIf this field is not specified, the server will default to ttigpro1.", + "label": "", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "string.max_len", + "value": 36 + }, + { + "name": "string.pattern", + "value": "^[a-z0-9](?:[-]?[a-z0-9]){2,}$|^$" + } + ] + } + }, + { + "name": "qr_code", + "description": "Raw QR code contents.", + "label": "", + "type": "bytes", + "longType": "bytes", + "fullType": "bytes", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "bytes.min_len", + "value": 10 + }, + { + "name": "bytes.max_len", + "value": 1024 + } + ] + } + } + ] + }, + { + "name": "ParseGatewayQRCodeResponse", + "longName": "ParseGatewayQRCodeResponse", + "fullName": "ttn.lorawan.v3.ParseGatewayQRCodeResponse", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "format_id", + "description": "Identifier of the format used to parse the QR code data.", + "label": "", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "gateway_eui", + "description": "", + "label": "", + "type": "bytes", + "longType": "bytes", + "fullType": "bytes", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "bytes.len", + "value": 8 + } + ] + } + }, + { + "name": "owner_token", + "description": "", + "label": "", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, { "name": "QRCodeFormat", "longName": "QRCodeFormat", @@ -48494,6 +48610,42 @@ } } ] + }, + { + "name": "GatewayQRCodeGenerator", + "longName": "GatewayQRCodeGenerator", + "fullName": "ttn.lorawan.v3.GatewayQRCodeGenerator", + "description": "The GatewayQRCodeGenerator service provides functionality to generate and parse QR codes for gateways.", + "methods": [ + { + "name": "Parse", + "description": "Parse QR Codes of known formats and return the information contained within.", + "requestType": "ParseGatewayQRCodeRequest", + "requestLongType": "ParseGatewayQRCodeRequest", + "requestFullType": "ttn.lorawan.v3.ParseGatewayQRCodeRequest", + "requestStreaming": false, + "responseType": "ParseGatewayQRCodeResponse", + "responseLongType": "ParseGatewayQRCodeResponse", + "responseFullType": "ttn.lorawan.v3.ParseGatewayQRCodeResponse", + "responseStreaming": false, + "options": { + "google.api.http": { + "rules": [ + { + "method": "POST", + "pattern": "/qr-codes/gateways/parse", + "body": "*" + }, + { + "method": "POST", + "pattern": "/qr-codes/gateways/{format_id}/parse", + "body": "*" + } + ] + } + } + } + ] } ] },