diff --git a/CHANGELOG.md b/CHANGELOG.md index ae76c08121..2b1476532f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ For details about compatibility between different releases, see the **Commitment - New `ListBands` RPC on the `Configuration` service. - Support for loading end device template from Device Repository when importing devices using a CSV file. +- Experimental support for normalized payload. ### Changed diff --git a/api/api.md b/api/api.md index 87d1faa951..e5e5d1879b 100644 --- a/api/api.md +++ b/api/api.md @@ -484,6 +484,8 @@ - [Message `ApplicationUp`](#ttn.lorawan.v3.ApplicationUp) - [Message `ApplicationUplink`](#ttn.lorawan.v3.ApplicationUplink) - [Message `ApplicationUplink.LocationsEntry`](#ttn.lorawan.v3.ApplicationUplink.LocationsEntry) + - [Message `ApplicationUplinkNormalized`](#ttn.lorawan.v3.ApplicationUplinkNormalized) + - [Message `ApplicationUplinkNormalized.LocationsEntry`](#ttn.lorawan.v3.ApplicationUplinkNormalized.LocationsEntry) - [Message `DownlinkMessage`](#ttn.lorawan.v3.DownlinkMessage) - [Message `DownlinkQueueOperationErrorDetails`](#ttn.lorawan.v3.DownlinkQueueOperationErrorDetails) - [Message `DownlinkQueueRequest`](#ttn.lorawan.v3.DownlinkQueueRequest) @@ -1522,6 +1524,7 @@ The ApplicationUpStorage service can be used to query stored application upstrea | `downlink_push` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | The topic to which the Application Server subscribes for downlink queue push operations. | | `downlink_replace` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | The topic to which the Application Server subscribes for downlink queue replace operations. | | `uplink_message` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | | +| `uplink_normalized` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | | | `join_accept` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | | | `downlink_ack` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | | | `downlink_nack` | [`ApplicationPubSub.Message`](#ttn.lorawan.v3.ApplicationPubSub.Message) | | | @@ -1780,6 +1783,7 @@ The NATS provider settings. | `template_fields` | [`ApplicationWebhook.TemplateFieldsEntry`](#ttn.lorawan.v3.ApplicationWebhook.TemplateFieldsEntry) | repeated | The value of the fields used by the template. Maps field.id to the value. | | `downlink_api_key` | [`string`](#string) | | The API key to be used for downlink queue operations. The field is provided for convenience reasons, and can contain API keys with additional rights (albeit this is discouraged). | | `uplink_message` | [`ApplicationWebhook.Message`](#ttn.lorawan.v3.ApplicationWebhook.Message) | | | +| `uplink_normalized` | [`ApplicationWebhook.Message`](#ttn.lorawan.v3.ApplicationWebhook.Message) | | | | `join_accept` | [`ApplicationWebhook.Message`](#ttn.lorawan.v3.ApplicationWebhook.Message) | | | | `downlink_ack` | [`ApplicationWebhook.Message`](#ttn.lorawan.v3.ApplicationWebhook.Message) | | | | `downlink_nack` | [`ApplicationWebhook.Message`](#ttn.lorawan.v3.ApplicationWebhook.Message) | | | @@ -1894,6 +1898,7 @@ The NATS provider settings. | `fields` | [`ApplicationWebhookTemplateField`](#ttn.lorawan.v3.ApplicationWebhookTemplateField) | repeated | | | `create_downlink_api_key` | [`bool`](#bool) | | Control the creation of the downlink queue operations API key. | | `uplink_message` | [`ApplicationWebhookTemplate.Message`](#ttn.lorawan.v3.ApplicationWebhookTemplate.Message) | | | +| `uplink_normalized` | [`ApplicationWebhookTemplate.Message`](#ttn.lorawan.v3.ApplicationWebhookTemplate.Message) | | | | `join_accept` | [`ApplicationWebhookTemplate.Message`](#ttn.lorawan.v3.ApplicationWebhookTemplate.Message) | | | | `downlink_ack` | [`ApplicationWebhookTemplate.Message`](#ttn.lorawan.v3.ApplicationWebhookTemplate.Message) | | | | `downlink_nack` | [`ApplicationWebhookTemplate.Message`](#ttn.lorawan.v3.ApplicationWebhookTemplate.Message) | | | @@ -6933,6 +6938,7 @@ Transmission settings for downlink. | Field | Validations | | ----- | ----------- | | `session_key_id` |

`bytes.max_len`: `2048`

| +| `received_at` |

`timestamp.required`: `true`

| ### Message `ApplicationLocation` @@ -6973,6 +6979,7 @@ Application uplink message. | `correlation_ids` | [`string`](#string) | repeated | | | `received_at` | [`google.protobuf.Timestamp`](#google.protobuf.Timestamp) | | Server time when the Application Server received the message. | | `uplink_message` | [`ApplicationUplink`](#ttn.lorawan.v3.ApplicationUplink) | | | +| `uplink_normalized` | [`ApplicationUplinkNormalized`](#ttn.lorawan.v3.ApplicationUplinkNormalized) | | | | `join_accept` | [`ApplicationJoinAccept`](#ttn.lorawan.v3.ApplicationJoinAccept) | | | | `downlink_ack` | [`ApplicationDownlink`](#ttn.lorawan.v3.ApplicationDownlink) | | | | `downlink_nack` | [`ApplicationDownlink`](#ttn.lorawan.v3.ApplicationDownlink) | | | @@ -6996,18 +7003,20 @@ Application uplink message. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | `session_key_id` | [`bytes`](#bytes) | | Join Server issued identifier for the session keys used by this uplink. | -| `f_port` | [`uint32`](#uint32) | | | -| `f_cnt` | [`uint32`](#uint32) | | | +| `f_port` | [`uint32`](#uint32) | | LoRaWAN FPort of the uplink message. | +| `f_cnt` | [`uint32`](#uint32) | | LoRaWAN FCntUp of the uplink message. | | `frm_payload` | [`bytes`](#bytes) | | The frame payload of the uplink message. The payload is still encrypted if the skip_payload_crypto field of the EndDevice is true, which is indicated by the presence of the app_s_key field. | | `decoded_payload` | [`google.protobuf.Struct`](#google.protobuf.Struct) | | The decoded frame payload of the uplink message. This field is set by the message processor that is configured for the end device (see formatters) or application (see default_formatters). | | `decoded_payload_warnings` | [`string`](#string) | repeated | Warnings generated by the message processor while decoding the frm_payload. | +| `normalized_payload` | [`google.protobuf.Struct`](#google.protobuf.Struct) | repeated | The normalized frame payload of the uplink message. This field is set by the message processor that is configured for the end device (see formatters) or application (see default_formatters). If the message processor is a custom script, there is no uplink normalizer script and the decoded output is valid normalized payload, this field contains the decoded payload. | +| `normalized_payload_warnings` | [`string`](#string) | repeated | Warnings generated by the message processor while normalizing the decoded payload. | | `rx_metadata` | [`RxMetadata`](#ttn.lorawan.v3.RxMetadata) | repeated | A list of metadata for each antenna of each gateway that received this message. | -| `settings` | [`TxSettings`](#ttn.lorawan.v3.TxSettings) | | Settings for the transmission. | +| `settings` | [`TxSettings`](#ttn.lorawan.v3.TxSettings) | | Transmission settings used by the end device. | | `received_at` | [`google.protobuf.Timestamp`](#google.protobuf.Timestamp) | | Server time when the Network Server received the message. | | `app_s_key` | [`KeyEnvelope`](#ttn.lorawan.v3.KeyEnvelope) | | The AppSKey of the current session. This field is only present if the skip_payload_crypto field of the EndDevice is true. Can be used to decrypt uplink payloads and encrypt downlink payloads. | | `last_a_f_cnt_down` | [`uint32`](#uint32) | | The last AFCntDown of the current session. This field is only present if the skip_payload_crypto field of the EndDevice is true. Can be used with app_s_key to encrypt downlink payloads. | -| `confirmed` | [`bool`](#bool) | | | -| `consumed_airtime` | [`google.protobuf.Duration`](#google.protobuf.Duration) | | Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the RawPayload size and the transmission settings. | +| `confirmed` | [`bool`](#bool) | | Indicates whether the end device used confirmed data uplink. | +| `consumed_airtime` | [`google.protobuf.Duration`](#google.protobuf.Duration) | | Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings. | | `locations` | [`ApplicationUplink.LocationsEntry`](#ttn.lorawan.v3.ApplicationUplink.LocationsEntry) | repeated | End device location metadata, set by the Application Server while handling the message. | | `version_ids` | [`EndDeviceVersionIdentifiers`](#ttn.lorawan.v3.EndDeviceVersionIdentifiers) | | End device version identifiers, set by the Application Server while handling the message. | | `network_ids` | [`NetworkIdentifiers`](#ttn.lorawan.v3.NetworkIdentifiers) | | Network identifiers, set by the Network Server that handles the message. | @@ -7027,6 +7036,42 @@ Application uplink message. | `key` | [`string`](#string) | | | | `value` | [`Location`](#ttn.lorawan.v3.Location) | | | +### Message `ApplicationUplinkNormalized` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `session_key_id` | [`bytes`](#bytes) | | Join Server issued identifier for the session keys used by this uplink. | +| `f_port` | [`uint32`](#uint32) | | LoRaWAN FPort of the uplink message. | +| `f_cnt` | [`uint32`](#uint32) | | LoRaWAN FCntUp of the uplink message. | +| `frm_payload` | [`bytes`](#bytes) | | The frame payload of the uplink message. This field is always decrypted with AppSKey. | +| `normalized_payload` | [`google.protobuf.Struct`](#google.protobuf.Struct) | | The normalized frame payload of the uplink message. This field is set for each item in normalized_payload in the corresponding ApplicationUplink message. | +| `normalized_payload_warnings` | [`string`](#string) | repeated | This field is set to normalized_payload_warnings in the corresponding ApplicationUplink message. | +| `rx_metadata` | [`RxMetadata`](#ttn.lorawan.v3.RxMetadata) | repeated | A list of metadata for each antenna of each gateway that received this message. | +| `settings` | [`TxSettings`](#ttn.lorawan.v3.TxSettings) | | Transmission settings used by the end device. | +| `received_at` | [`google.protobuf.Timestamp`](#google.protobuf.Timestamp) | | Server time when the Network Server received the message. | +| `confirmed` | [`bool`](#bool) | | Indicates whether the end device used confirmed data uplink. | +| `consumed_airtime` | [`google.protobuf.Duration`](#google.protobuf.Duration) | | Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings. | +| `locations` | [`ApplicationUplinkNormalized.LocationsEntry`](#ttn.lorawan.v3.ApplicationUplinkNormalized.LocationsEntry) | repeated | End device location metadata, set by the Application Server while handling the message. | +| `version_ids` | [`EndDeviceVersionIdentifiers`](#ttn.lorawan.v3.EndDeviceVersionIdentifiers) | | End device version identifiers, set by the Application Server while handling the message. | +| `network_ids` | [`NetworkIdentifiers`](#ttn.lorawan.v3.NetworkIdentifiers) | | Network identifiers, set by the Network Server that handles the message. | + +#### Field Rules + +| Field | Validations | +| ----- | ----------- | +| `session_key_id` |

`bytes.max_len`: `2048`

| +| `f_port` |

`uint32.lte`: `255`

`uint32.gte`: `1`

`uint32.not_in`: `[224]`

| +| `normalized_payload` |

`message.required`: `true`

| +| `settings` |

`message.required`: `true`

| +| `received_at` |

`timestamp.required`: `true`

| + +### Message `ApplicationUplinkNormalized.LocationsEntry` + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `key` | [`string`](#string) | | | +| `value` | [`Location`](#ttn.lorawan.v3.Location) | | | + ### Message `DownlinkMessage` Downlink message from the network to the end device diff --git a/api/api.swagger.json b/api/api.swagger.json index a5a9359d33..ebe3d561d5 100644 --- a/api/api.swagger.json +++ b/api/api.swagger.json @@ -4166,6 +4166,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationUplink" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationUplinkNormalized" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationJoinAccept" }, @@ -4701,6 +4704,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationPubSubMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationPubSubMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationPubSubMessage" }, @@ -4826,6 +4832,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationPubSubMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationPubSubMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationPubSubMessage" }, @@ -5156,6 +5165,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationWebhookMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationWebhookMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationWebhookMessage" }, @@ -5292,6 +5304,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationWebhookMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationWebhookMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationWebhookMessage" }, @@ -19219,6 +19234,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationPubSubMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationPubSubMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationPubSubMessage" }, @@ -19322,6 +19340,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationUplink" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationUplinkNormalized" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationJoinAccept" }, @@ -19366,11 +19387,13 @@ }, "f_port": { "type": "integer", - "format": "int64" + "format": "int64", + "description": "LoRaWAN FPort of the uplink message." }, "f_cnt": { "type": "integer", - "format": "int64" + "format": "int64", + "description": "LoRaWAN FCntUp of the uplink message." }, "frm_payload": { "type": "string", @@ -19388,6 +19411,20 @@ }, "description": "Warnings generated by the message processor while decoding the frm_payload." }, + "normalized_payload": { + "type": "array", + "items": { + "type": "object" + }, + "description": "The normalized frame payload of the uplink message.\nThis field is set by the message processor that is configured for the end device (see formatters) or application (see default_formatters).\nIf the message processor is a custom script, there is no uplink normalizer script and the decoded output is valid\nnormalized payload, this field contains the decoded payload." + }, + "normalized_payload_warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Warnings generated by the message processor while normalizing the decoded payload." + }, "rx_metadata": { "type": "array", "items": { @@ -19397,7 +19434,7 @@ }, "settings": { "$ref": "#/definitions/lorawanv3TxSettings", - "description": "Settings for the transmission." + "description": "Transmission settings used by the end device." }, "received_at": { "type": "string", @@ -19414,11 +19451,87 @@ "description": "The last AFCntDown of the current session.\nThis field is only present if the skip_payload_crypto field of the EndDevice\nis true.\nCan be used with app_s_key to encrypt downlink payloads." }, "confirmed": { - "type": "boolean" + "type": "boolean", + "description": "Indicates whether the end device used confirmed data uplink." }, "consumed_airtime": { "type": "string", - "description": "Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the RawPayload size and the transmission settings." + "description": "Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings." + }, + "locations": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/lorawanv3Location" + }, + "description": "End device location metadata, set by the Application Server while handling the message." + }, + "version_ids": { + "$ref": "#/definitions/v3EndDeviceVersionIdentifiers", + "description": "End device version identifiers, set by the Application Server while handling the message." + }, + "network_ids": { + "$ref": "#/definitions/v3NetworkIdentifiers", + "description": "Network identifiers, set by the Network Server that handles the message." + } + } + }, + "v3ApplicationUplinkNormalized": { + "type": "object", + "properties": { + "session_key_id": { + "type": "string", + "format": "byte", + "description": "Join Server issued identifier for the session keys used by this uplink." + }, + "f_port": { + "type": "integer", + "format": "int64", + "description": "LoRaWAN FPort of the uplink message." + }, + "f_cnt": { + "type": "integer", + "format": "int64", + "description": "LoRaWAN FCntUp of the uplink message." + }, + "frm_payload": { + "type": "string", + "format": "byte", + "description": "The frame payload of the uplink message.\nThis field is always decrypted with AppSKey." + }, + "normalized_payload": { + "type": "object", + "description": "The normalized frame payload of the uplink message.\nThis field is set for each item in normalized_payload in the corresponding ApplicationUplink message." + }, + "normalized_payload_warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "This field is set to normalized_payload_warnings in the corresponding ApplicationUplink message." + }, + "rx_metadata": { + "type": "array", + "items": { + "$ref": "#/definitions/lorawanv3RxMetadata" + }, + "description": "A list of metadata for each antenna of each gateway that received this message." + }, + "settings": { + "$ref": "#/definitions/lorawanv3TxSettings", + "description": "Transmission settings used by the end device." + }, + "received_at": { + "type": "string", + "format": "date-time", + "description": "Server time when the Network Server received the message." + }, + "confirmed": { + "type": "boolean", + "description": "Indicates whether the end device used confirmed data uplink." + }, + "consumed_airtime": { + "type": "string", + "description": "Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings." }, "locations": { "type": "object", @@ -19484,6 +19597,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationWebhookMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationWebhookMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationWebhookMessage" }, @@ -19610,6 +19726,9 @@ "uplink_message": { "$ref": "#/definitions/v3ApplicationWebhookTemplateMessage" }, + "uplink_normalized": { + "$ref": "#/definitions/v3ApplicationWebhookTemplateMessage" + }, "join_accept": { "$ref": "#/definitions/v3ApplicationWebhookTemplateMessage" }, diff --git a/api/applicationserver_pubsub.proto b/api/applicationserver_pubsub.proto index 75068f880f..b106088b25 100644 --- a/api/applicationserver_pubsub.proto +++ b/api/applicationserver_pubsub.proto @@ -174,6 +174,7 @@ message ApplicationPubSub { Message downlink_replace = 8; Message uplink_message = 9; + Message uplink_normalized = 20; Message join_accept = 10; Message downlink_ack = 11; Message downlink_nack = 12; @@ -184,7 +185,7 @@ message ApplicationPubSub { Message location_solved = 16; Message service_data = 18; - // next: 20 + // next: 21 } message ApplicationPubSubs { diff --git a/api/applicationserver_web.proto b/api/applicationserver_web.proto index d7725f04ab..9976f79a8b 100644 --- a/api/applicationserver_web.proto +++ b/api/applicationserver_web.proto @@ -88,6 +88,7 @@ message ApplicationWebhookTemplate { string path = 1 [(validate.rules).string.max_len = 64]; } Message uplink_message = 11; + Message uplink_normalized = 23; Message join_accept = 12; Message downlink_ack = 13; Message downlink_nack = 14; @@ -100,7 +101,7 @@ message ApplicationWebhookTemplate { google.protobuf.FieldMask field_mask = 22; - // next: 23 + // next: 24 } message ApplicationWebhookTemplates { @@ -166,6 +167,7 @@ message ApplicationWebhook { string path = 1 [(validate.rules).string.max_len = 64]; } Message uplink_message = 7; + Message uplink_normalized = 22; Message join_accept = 8; Message downlink_ack = 9; Message downlink_nack = 10; @@ -180,7 +182,7 @@ message ApplicationWebhook { google.protobuf.FieldMask field_mask = 21; - // next: 22 + // next: 23 } message ApplicationWebhooks { diff --git a/api/messages.proto b/api/messages.proto index 7ee9f3d4c7..515908d101 100644 --- a/api/messages.proto +++ b/api/messages.proto @@ -190,7 +190,9 @@ message ApplicationUplink { option (thethings.flags.message) = { select: true, set: false }; // Join Server issued identifier for the session keys used by this uplink. bytes session_key_id = 1 [(validate.rules).bytes.max_len = 2048]; + // LoRaWAN FPort of the uplink message. uint32 f_port = 2 [(validate.rules).uint32 = {lte: 255, not_in: [224]}]; + // LoRaWAN FCntUp of the uplink message. uint32 f_cnt = 3; // The frame payload of the uplink message. @@ -204,10 +206,18 @@ message ApplicationUplink { // Warnings generated by the message processor while decoding the frm_payload. repeated string decoded_payload_warnings = 12; + // The normalized frame payload of the uplink message. + // This field is set by the message processor that is configured for the end device (see formatters) or application (see default_formatters). + // If the message processor is a custom script, there is no uplink normalizer script and the decoded output is valid + // normalized payload, this field contains the decoded payload. + repeated google.protobuf.Struct normalized_payload = 17; + // Warnings generated by the message processor while normalizing the decoded payload. + repeated string normalized_payload_warnings = 18; + // A list of metadata for each antenna of each gateway that received this message. repeated RxMetadata rx_metadata = 6; - // Settings for the transmission. + // Transmission settings used by the end device. TxSettings settings = 7 [(validate.rules).message.required = true]; // Server time when the Network Server received the message. @@ -224,9 +234,10 @@ message ApplicationUplink { // Can be used with app_s_key to encrypt downlink payloads. uint32 last_a_f_cnt_down = 10; + // Indicates whether the end device used confirmed data uplink. bool confirmed = 11; - // Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the RawPayload size and the transmission settings. + // Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings. google.protobuf.Duration consumed_airtime = 13; // End device location metadata, set by the Application Server while handling the message. @@ -237,6 +248,54 @@ message ApplicationUplink { // Network identifiers, set by the Network Server that handles the message. NetworkIdentifiers network_ids = 16; + + // next: 19 +} + +message ApplicationUplinkNormalized { + option (thethings.flags.message) = { select: true, set: false }; + // Join Server issued identifier for the session keys used by this uplink. + bytes session_key_id = 1 [(validate.rules).bytes.max_len = 2048]; + // LoRaWAN FPort of the uplink message. + uint32 f_port = 2 [(validate.rules).uint32 = {gte: 1, lte: 255, not_in: [224]}]; + // LoRaWAN FCntUp of the uplink message. + uint32 f_cnt = 3; + + // The frame payload of the uplink message. + // This field is always decrypted with AppSKey. + bytes frm_payload = 4; + + // The normalized frame payload of the uplink message. + // This field is set for each item in normalized_payload in the corresponding ApplicationUplink message. + google.protobuf.Struct normalized_payload = 5 [(validate.rules).message.required = true]; + // This field is set to normalized_payload_warnings in the corresponding ApplicationUplink message. + repeated string normalized_payload_warnings = 6; + + // A list of metadata for each antenna of each gateway that received this message. + repeated RxMetadata rx_metadata = 7; + + // Transmission settings used by the end device. + TxSettings settings = 8 [(validate.rules).message.required = true]; + + // Server time when the Network Server received the message. + google.protobuf.Timestamp received_at = 9 [(validate.rules).timestamp.required = true]; + + // Indicates whether the end device used confirmed data uplink. + bool confirmed = 10; + + // Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings. + google.protobuf.Duration consumed_airtime = 11; + + // End device location metadata, set by the Application Server while handling the message. + map locations = 12; + + // End device version identifiers, set by the Application Server while handling the message. + EndDeviceVersionIdentifiers version_ids = 13; + + // Network identifiers, set by the Network Server that handles the message. + NetworkIdentifiers network_ids = 14; + + // next: 15 } message ApplicationLocation { @@ -264,7 +323,7 @@ message ApplicationJoinAccept { // rejoin-request. bool pending_session = 4; // Server time when the Network Server received the message. - google.protobuf.Timestamp received_at = 8; + google.protobuf.Timestamp received_at = 8 [(validate.rules).timestamp.required = true]; } message ApplicationDownlink { @@ -389,6 +448,7 @@ message ApplicationUp { option (validate.required) = true; ApplicationUplink uplink_message = 3; + ApplicationUplinkNormalized uplink_normalized = 15; ApplicationJoinAccept join_accept = 4; ApplicationDownlink downlink_ack = 5; ApplicationDownlink downlink_nack = 6; @@ -402,6 +462,8 @@ message ApplicationUp { // Signals if the message is coming from the Network Server or is simulated. bool simulated = 14; + + // next: 17 } enum PayloadFormatter { diff --git a/config/messages.json b/config/messages.json index 74311d334b..e4ca5c2d83 100644 --- a/config/messages.json +++ b/config/messages.json @@ -6938,6 +6938,60 @@ "file": "javascript.go" } }, + "error:pkg/messageprocessors/normalizedpayload:field_exclusive_maximum": { + "translations": { + "en": "`{path}` should be less than `{maximum}`" + }, + "description": { + "package": "pkg/messageprocessors/normalizedpayload", + "file": "uplink.go" + } + }, + "error:pkg/messageprocessors/normalizedpayload:field_exclusive_minimum": { + "translations": { + "en": "`{path}` should be greater than `{minimum}`" + }, + "description": { + "package": "pkg/messageprocessors/normalizedpayload", + "file": "uplink.go" + } + }, + "error:pkg/messageprocessors/normalizedpayload:field_maximum": { + "translations": { + "en": "`{path}` should be equal or less than `{maximum}`" + }, + "description": { + "package": "pkg/messageprocessors/normalizedpayload", + "file": "uplink.go" + } + }, + "error:pkg/messageprocessors/normalizedpayload:field_minimum": { + "translations": { + "en": "`{path}` should be equal or greater than `{minimum}`" + }, + "description": { + "package": "pkg/messageprocessors/normalizedpayload", + "file": "uplink.go" + } + }, + "error:pkg/messageprocessors/normalizedpayload:field_type": { + "translations": { + "en": "invalid field type of `{path}`" + }, + "description": { + "package": "pkg/messageprocessors/normalizedpayload", + "file": "uplink.go" + } + }, + "error:pkg/messageprocessors/normalizedpayload:unknown_field": { + "translations": { + "en": "unknown field `{path}`" + }, + "description": { + "package": "pkg/messageprocessors/normalizedpayload", + "file": "uplink.go" + } + }, "error:pkg/messageprocessors:formatter_not_configured": { "translations": { "en": "formatter `{formatter}` is not configured" @@ -9260,6 +9314,15 @@ "file": "observability.go" } }, + "event:as.up.data.normalize.warning": { + "translations": { + "en": "normalize uplink data message warning" + }, + "description": { + "package": "pkg/applicationserver", + "file": "observability.go" + } + }, "event:as.up.data.receive": { "translations": { "en": "receive uplink data message" @@ -9305,6 +9368,15 @@ "file": "observability.go" } }, + "event:as.up.normalized.forward": { + "translations": { + "en": "forward normalized uplink message" + }, + "description": { + "package": "pkg/applicationserver", + "file": "observability.go" + } + }, "event:as.up.service.forward": { "translations": { "en": "forward service data message" diff --git a/cypress/integration/console/integrations/webhooks/create-with-template.spec.js b/cypress/integration/console/integrations/webhooks/create-with-template.spec.js index 9edeb48036..c6336d833b 100644 --- a/cypress/integration/console/integrations/webhooks/create-with-template.spec.js +++ b/cypress/integration/console/integrations/webhooks/create-with-template.spec.js @@ -37,7 +37,7 @@ describe('Application Webhook create', () => { cy.loginConsole({ user_id: userId, password: user.password }) cy.intercept( 'GET', - `/api/v3/as/webhook-templates?field_mask=base_url,create_downlink_api_key,description,documentation_url,downlink_ack,downlink_failed,downlink_nack,downlink_queue_invalidated,downlink_queued,downlink_sent,fields,format,headers,ids,info_url,join_accept,location_solved,logo_url,name,service_data,uplink_message`, + `/api/v3/as/webhook-templates?field_mask=base_url,create_downlink_api_key,description,documentation_url,downlink_ack,downlink_failed,downlink_nack,downlink_queue_invalidated,downlink_queued,downlink_sent,fields,format,headers,ids,info_url,join_accept,location_solved,logo_url,name,service_data,uplink_message,uplink_normalized`, { fixture: 'console/application/integrations/webhook/template.json' }, ) cy.visit( diff --git a/go.mod b/go.mod index 87ea8f6617..608cc11c17 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/blevesearch/bleve v1.0.14 github.com/bluele/gcache v0.0.2 github.com/disintegration/imaging v1.6.2 - github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f + github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6 github.com/dustin/go-humanize v1.0.0 github.com/eclipse/paho.mqtt.golang v1.3.5 github.com/emersion/go-smtp v0.15.0 @@ -180,7 +180,7 @@ require ( github.com/couchbase/vellum v1.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-kit/log v0.2.0 // indirect diff --git a/go.sum b/go.sum index 2079d24941..cd6f8f4dbf 100644 --- a/go.sum +++ b/go.sum @@ -193,9 +193,15 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f h1:ztRywKO1rqqS8li0TDcnwi9AGsqAH0ky9NaND69/Ccc= github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6 h1:xHdUVG+c8SWJnct16Z3QJOVlaYo3OwoJyamo6kR6OL0= +github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= diff --git a/pkg/applicationserver/applicationserver.go b/pkg/applicationserver/applicationserver.go index dec319150c..5ff4576b82 100644 --- a/pkg/applicationserver/applicationserver.go +++ b/pkg/applicationserver/applicationserver.go @@ -124,10 +124,7 @@ func New(c *component.Component, conf *Config) (as *ApplicationServer, err error deviceRegistry: wrapEndDeviceRegistryWithReplacedFields(conf.Devices, replacedEndDeviceFields...), appUpsRegistry: conf.UplinkStorage.Registry, locationRegistry: conf.EndDeviceMetadataStorage.Location.Registry, - formatters: messageprocessors.MapPayloadProcessor{ - ttnpb.PayloadFormatter_FORMATTER_JAVASCRIPT: javascript.New(), - ttnpb.PayloadFormatter_FORMATTER_CAYENNELPP: cayennelpp.New(), - }, + formatters: make(messageprocessors.MapPayloadProcessor), clusterDistributor: distribution.NewPubSubDistributor( ctx, c, @@ -145,11 +142,16 @@ func New(c *component.Component, conf *Config) (as *ApplicationServer, err error interopClient: interopCl, interopID: conf.Interop.ID, } + + as.formatters[ttnpb.PayloadFormatter_FORMATTER_JAVASCRIPT] = javascript.New() + as.formatters[ttnpb.PayloadFormatter_FORMATTER_CAYENNELPP] = cayennelpp.New() + as.formatters[ttnpb.PayloadFormatter_FORMATTER_REPOSITORY] = devicerepository.New(as.formatters, as) + as.activationPool = workerpool.NewWorkerPool(workerpool.Config[*ttnpb.EndDeviceIdentifiers]{ Component: c, Context: ctx, - Name: "save_activation_status", - Handler: as.saveActivationStatus, + Name: "set_activated", + Handler: as.setActivated, }) as.processingPool = workerpool.NewWorkerPool(workerpool.Config[*ttnpb.ApplicationUp]{ Component: c, @@ -157,16 +159,13 @@ func New(c *component.Component, conf *Config) (as *ApplicationServer, err error Name: "process_application_uplinks", Handler: as.processUpAsync, }) - as.deviceLastSeenPool = workerpool.NewWorkerPool(workerpool.Config[lastSeenAtInfo]{ Component: c, Context: ctx, - Name: "save_device_last_seen_from_uplink", - Handler: as.processDeviceLastSeenAsync, + Name: "store_device_last_seen_from_uplink", + Handler: as.storeDeviceLastSeen, }) - as.formatters[ttnpb.PayloadFormatter_FORMATTER_REPOSITORY] = devicerepository.New(as.formatters, as) - as.grpc.asDevices = asEndDeviceRegistryServer{ AS: as, kekLabel: conf.DeviceKEKLabel, @@ -282,24 +281,24 @@ func (as *ApplicationServer) RegisterServices(s *grpc.Server) { if ps := as.pubsub; ps != nil { ttnpb.RegisterApplicationPubSubRegistryServer(s, ps) } - if packages := as.appPackages; packages != nil { - packages.RegisterServices(s) + if pkgs := as.appPackages; pkgs != nil { + pkgs.RegisterServices(s) } } // RegisterHandlers registers gRPC handlers. func (as *ApplicationServer) RegisterHandlers(s *runtime.ServeMux, conn *grpc.ClientConn) { - ttnpb.RegisterAsHandler(as.Context(), s, conn) - ttnpb.RegisterAsEndDeviceRegistryHandler(as.Context(), s, conn) - ttnpb.RegisterAppAsHandler(as.Context(), s, conn) + ttnpb.RegisterAsHandler(as.Context(), s, conn) //nolint:errcheck + ttnpb.RegisterAsEndDeviceRegistryHandler(as.Context(), s, conn) //nolint:errcheck + ttnpb.RegisterAppAsHandler(as.Context(), s, conn) //nolint:errcheck if as.webhooks != nil { - ttnpb.RegisterApplicationWebhookRegistryHandler(as.Context(), s, conn) + ttnpb.RegisterApplicationWebhookRegistryHandler(as.Context(), s, conn) //nolint:errcheck } if as.pubsub != nil { - ttnpb.RegisterApplicationPubSubRegistryHandler(as.Context(), s, conn) + ttnpb.RegisterApplicationPubSubRegistryHandler(as.Context(), s, conn) //nolint:errcheck } - if packages := as.appPackages; packages != nil { - packages.RegisterHandlers(s, conn) + if pkgs := as.appPackages; pkgs != nil { + pkgs.RegisterHandlers(s, conn) } } @@ -308,13 +307,13 @@ func (as *ApplicationServer) RegisterRoutes(s *web.Server) { if wh := as.webhooks; wh != nil { wh.RegisterRoutes(s) } - if packages := as.appPackages; packages != nil { - packages.RegisterRoutes(s) + if pkgs := as.appPackages; pkgs != nil { + pkgs.RegisterRoutes(s) } } // Roles returns the roles that the Application Server fulfills. -func (as *ApplicationServer) Roles() []ttnpb.ClusterRole { +func (*ApplicationServer) Roles() []ttnpb.ClusterRole { return []ttnpb.ClusterRole{ttnpb.ClusterRole_APPLICATION_SERVER} } @@ -359,10 +358,9 @@ type lastSeenAtInfo struct { lastSeenAt *pbtypes.Timestamp } -func (as *ApplicationServer) processDeviceLastSeenAsync(ctx context.Context, lastSeenEntry lastSeenAtInfo) { +func (as *ApplicationServer) storeDeviceLastSeen(ctx context.Context, lastSeenEntry lastSeenAtInfo) { if err := as.deviceLastSeenProvider.PushLastSeenFromUplink(ctx, lastSeenEntry.ids, lastSeenEntry.lastSeenAt); err != nil { - log.FromContext(ctx).WithError(err).Warn("Failed to update device last seen timestamp") - return + log.FromContext(ctx).WithError(err).Warn("Failed to set device last seen timestamp") } } @@ -374,8 +372,6 @@ func (as *ApplicationServer) processUp(ctx context.Context, up *ttnpb.Applicatio up.CorrelationIds = events.CorrelationIDsFromContext(ctx) registerReceiveUp(ctx, up) - up.ReceivedAt = ttnpb.ProtoTimePtr(time.Now()) - pass, err := as.handleUp(ctx, up, link) if err != nil { log.FromContext(ctx).WithError(err).Warn("Failed to process upstream message") @@ -459,8 +455,8 @@ func (as *ApplicationServer) buildSessionsFromError(ctx context.Context, dev *tt }, nil } - ttnErr, ok := err.(errors.ErrorDetails) - if !ok { + var ttnErr errors.ErrorDetails + if !errors.As(err, &ttnErr) { return nil, err } details := ttnErr.Details() @@ -469,6 +465,7 @@ func (as *ApplicationServer) buildSessionsFromError(ctx context.Context, dev *tt } var diagnostics *ttnpb.DownlinkQueueOperationErrorDetails for _, detail := range details { + var ok bool diagnostics, ok = detail.(*ttnpb.DownlinkQueueOperationErrorDetails) if ok { break @@ -799,7 +796,9 @@ func (as *ApplicationServer) handleUp(ctx context.Context, up *ttnpb.Application case *ttnpb.ApplicationUp_JoinAccept: return true, as.handleJoinAccept(ctx, up.EndDeviceIds, p.JoinAccept, link) case *ttnpb.ApplicationUp_UplinkMessage: - return true, as.handleUplink(ctx, up.EndDeviceIds, p.UplinkMessage, link) + return true, as.handleUplink(ctx, uplinkInfo{up.EndDeviceIds, up.ReceivedAt, p.UplinkMessage, false, link}) + case *ttnpb.ApplicationUp_UplinkNormalized: + return true, nil case *ttnpb.ApplicationUp_DownlinkQueueInvalidated: return as.handleDownlinkQueueInvalidated(ctx, up.EndDeviceIds, p.DownlinkQueueInvalidated, link) case *ttnpb.ApplicationUp_DownlinkSent: @@ -822,7 +821,7 @@ func (as *ApplicationServer) handleUp(ctx context.Context, up *ttnpb.Application func (as *ApplicationServer) handleSimulatedUp(ctx context.Context, up *ttnpb.ApplicationUp, link *ttnpb.ApplicationLink) error { switch p := up.Up.(type) { case *ttnpb.ApplicationUp_UplinkMessage: - return as.handleSimulatedUplink(ctx, up.EndDeviceIds, p.UplinkMessage, link) + return as.handleSimulatedUplink(ctx, uplinkInfo{up.EndDeviceIds, up.ReceivedAt, p.UplinkMessage, true, link}) default: return nil } @@ -833,7 +832,7 @@ var errFetchAppSKey = errors.Define("app_s_key", "failed to get AppSKey") // handleJoinAccept handles a join-accept message. // If the application or device is not configured to skip application crypto, the InvalidatedDownlinks and the AppSKey // in the given join-accept message is reset. -func (as *ApplicationServer) handleJoinAccept(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, joinAccept *ttnpb.ApplicationJoinAccept, link *ttnpb.ApplicationLink) (err error) { +func (as *ApplicationServer) handleJoinAccept(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, joinAccept *ttnpb.ApplicationJoinAccept, link *ttnpb.ApplicationLink) error { defer trace.StartRegion(ctx, "handle join accept").End() logger := log.FromContext(ctx).WithFields(log.Fields( @@ -845,6 +844,7 @@ func (as *ApplicationServer) handleJoinAccept(ctx context.Context, ids *ttnpb.En if err != nil { return err } + _, err = as.deviceRegistry.Set(ctx, ids, []string{ "formatters", @@ -927,10 +927,19 @@ func (as *ApplicationServer) handleJoinAccept(ctx context.Context, ids *ttnpb.En return dev, mask, nil }, ) - if err == nil { - as.deviceLastSeenPool.Publish(ctx, lastSeenAtInfo{ids: ids, lastSeenAt: joinAccept.ReceivedAt}) + if err != nil { + return err } - return err + + // Publish last seen event. + if err := as.deviceLastSeenPool.Publish(ctx, lastSeenAtInfo{ + ids: ids, + lastSeenAt: joinAccept.ReceivedAt, + }); err != nil { + logger.WithError(err).Warn("Failed to publish last seen event") + } + + return nil } var errUnknownSession = errors.DefineNotFound("unknown_session", "unknown session") @@ -939,9 +948,9 @@ var errUnknownSession = errors.DefineNotFound("unknown_session", "unknown sessio // This function will mutate the provided ttnpb.EndDevice and migrate the Session field to the session that matches // the provided session key ID. // The following fields are expected to be part of the provided ttnpb.EndDevice: -// - session and pending_session - used to decide which session is currently active. -// - formatters, version_ids - used by the downlink queue encoders, in cases in which the queue must be recalculated. -// - skip_payload_crypto_override - used by the downlink queue migration mechanism in order to avoid payload encryption. +// - session and pending_session, used to decide which session is currently active. +// - formatters, version_ids, used by the downlink queue encoders, in cases in which the queue must be recalculated. +// - skip_payload_crypto_override, used by the downlink queue migration mechanism in order to avoid payload encryption. func (as *ApplicationServer) matchSession(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, dev *ttnpb.EndDevice, link *ttnpb.ApplicationLink, sessionKeyID []byte) ([]string, error) { logger := log.FromContext(ctx) var mask []string @@ -976,8 +985,12 @@ func (as *ApplicationServer) matchSession(ctx context.Context, ids *ttnpb.EndDev // Only fields which are used by integrations are stored. // The fields which are stored are based on the following usages: // - io/packages/loragls/v3/package.go#multiFrameQuery -// - io/packages/loragls/v3/api/objects.go#parseRxMetadata -func (as *ApplicationServer) storeUplink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, uplink *ttnpb.ApplicationUplink) error { +// - io/packages/loragls/v3/api/objects.go#parseRxMetadata. +func (as *ApplicationServer) storeUplink( + ctx context.Context, + ids *ttnpb.EndDeviceIdentifiers, + uplink *ttnpb.ApplicationUplink, +) error { cleanUplink := &ttnpb.ApplicationUplink{ RxMetadata: make([]*ttnpb.RxMetadata, 0, len(uplink.RxMetadata)), ReceivedAt: uplink.ReceivedAt, @@ -1000,11 +1013,11 @@ func (as *ApplicationServer) storeUplink(ctx context.Context, ids *ttnpb.EndDevi return as.appUpsRegistry.Push(ctx, ids, cleanUplink) } -// saveActivationStatus attempts to mark the end device as activated in the Entity Registry. +// setActivated attempts to mark the end device as activated in the Entity Registry. // If the update succeeds, the end device will be updated in the Application Server end device registry // in order to avoid subsequent calls. -func (as *ApplicationServer) saveActivationStatus(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers) { - defer trace.StartRegion(ctx, "save activation status").End() +func (as *ApplicationServer) setActivated(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers) { + defer trace.StartRegion(ctx, "set activated").End() cc, err := as.GetPeerConn(ctx, ttnpb.ClusterRole_ENTITY_REGISTRY, nil) if err != nil { @@ -1038,14 +1051,51 @@ func (as *ApplicationServer) saveActivationStatus(ctx context.Context, ids *ttnp } } -func (as *ApplicationServer) handleUplink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, uplink *ttnpb.ApplicationUplink, link *ttnpb.ApplicationLink) (err error) { +func (as *ApplicationServer) publishNormalizedUplink(ctx context.Context, info uplinkInfo) error { + for _, measurement := range info.uplink.NormalizedPayload { + if err := as.Publish(ctx, &ttnpb.ApplicationUp{ + EndDeviceIds: info.ids, + CorrelationIds: events.CorrelationIDsFromContext(ctx), + ReceivedAt: info.receivedAt, + Up: &ttnpb.ApplicationUp_UplinkNormalized{ + UplinkNormalized: &ttnpb.ApplicationUplinkNormalized{ + SessionKeyId: info.uplink.SessionKeyId, + FPort: info.uplink.FPort, + FCnt: info.uplink.FCnt, + FrmPayload: info.uplink.FrmPayload, + NormalizedPayload: measurement, + NormalizedPayloadWarnings: info.uplink.NormalizedPayloadWarnings, + RxMetadata: info.uplink.RxMetadata, + Settings: info.uplink.Settings, + ReceivedAt: info.uplink.ReceivedAt, + Confirmed: info.uplink.Confirmed, + ConsumedAirtime: info.uplink.ConsumedAirtime, + Locations: info.uplink.Locations, + VersionIds: info.uplink.VersionIds, + NetworkIds: info.uplink.NetworkIds, + }, + }, + Simulated: info.simulated, + }); err != nil { + return err + } + } + return nil +} + +type uplinkInfo struct { + ids *ttnpb.EndDeviceIdentifiers + receivedAt *pbtypes.Timestamp + uplink *ttnpb.ApplicationUplink + simulated bool + link *ttnpb.ApplicationLink +} + +func (as *ApplicationServer) handleUplink(ctx context.Context, info uplinkInfo) error { defer trace.StartRegion(ctx, "handle uplink").End() - ctx = log.NewContextWithField(ctx, "session_key_id", uplink.SessionKeyId) - if uplink.ReceivedAt == nil { - panic("no NS timestamp in ApplicationUplink") - } - dev, err := as.deviceRegistry.Set(ctx, ids, + ctx = log.NewContextWithField(ctx, "session_key_id", info.uplink.SessionKeyId) + dev, err := as.deviceRegistry.Set(ctx, info.ids, []string{ "activated_at", "formatters", @@ -1056,9 +1106,9 @@ func (as *ApplicationServer) handleUplink(ctx context.Context, ids *ttnpb.EndDev }, func(dev *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { if dev == nil { - return nil, nil, errDeviceNotFound.WithAttributes("device_uid", unique.ID(ctx, ids)) + return nil, nil, errDeviceNotFound.WithAttributes("device_uid", unique.ID(ctx, info.ids)) } - mask, err := as.matchSession(ctx, ids, dev, link, uplink.SessionKeyId) + mask, err := as.matchSession(ctx, info.ids, dev, info.link, info.uplink.SessionKeyId) if err != nil { return nil, nil, err } @@ -1071,40 +1121,44 @@ func (as *ApplicationServer) handleUplink(ctx context.Context, ids *ttnpb.EndDev if err != nil { return err } - if !as.skipPayloadCrypto(ctx, link, dev, dev.Session) { - if err := as.decryptAndDecodeUplink(ctx, dev, uplink, link.DefaultFormatters); err != nil { + + if !as.skipPayloadCrypto(ctx, info.link, dev, dev.Session) { + if err := as.decryptAndDecodeUplink(ctx, dev, info.uplink, info.link.DefaultFormatters); err != nil { + return err + } + if err := as.publishNormalizedUplink(ctx, info); err != nil { return err } - if err := as.storeUplink(ctx, ids, uplink); err != nil { + if err := as.storeUplink(ctx, info.ids, info.uplink); err != nil { return err } } else if appSKey := dev.GetSession().GetKeys().GetAppSKey(); appSKey != nil { - uplink.AppSKey = appSKey - uplink.LastAFCntDown = dev.Session.LastAFCntDown + info.uplink.AppSKey = appSKey + info.uplink.LastAFCntDown = dev.Session.LastAFCntDown } - registerUplinkLatency(ctx, uplink) + registerUplinkLatency(ctx, info.uplink) if dev.VersionIds != nil { - uplink.VersionIds = dev.VersionIds + info.uplink.VersionIds = dev.VersionIds } - if locations, err := as.locationRegistry.Get(ctx, ids); err != nil { + // Set location in message and publish location solved if the payload contains location information. + if locations, err := as.locationRegistry.Get(ctx, info.ids); err != nil { log.FromContext(ctx).WithError(err).Warn("Failed to retrieve end device locations") } else { - uplink.Locations = locations + info.uplink.Locations = locations } - - loc := as.locationFromDecodedPayload(uplink) + loc := as.locationFromPayload(info.uplink) if loc != nil { - if uplink.Locations == nil { - uplink.Locations = make(map[string]*ttnpb.Location, 1) + if info.uplink.Locations == nil { + info.uplink.Locations = make(map[string]*ttnpb.Location, 1) } - uplink.Locations["frm-payload"] = loc + info.uplink.Locations["frm-payload"] = loc if err := as.Publish(ctx, &ttnpb.ApplicationUp{ - EndDeviceIds: ids, + EndDeviceIds: info.ids, CorrelationIds: events.CorrelationIDsFromContext(ctx), - ReceivedAt: uplink.ReceivedAt, + ReceivedAt: info.receivedAt, Up: &ttnpb.ApplicationUp_LocationSolved{ LocationSolved: &ttnpb.ApplicationLocation{ Service: "frm-payload", @@ -1116,22 +1170,29 @@ func (as *ApplicationServer) handleUplink(ctx context.Context, ids *ttnpb.EndDev } } + // If the device has not been activated before, publish the activation event. if dev.ActivatedAt == nil { - as.activationPool.Publish(ctx, ids) + if err := as.activationPool.Publish(ctx, info.ids); err != nil { + log.FromContext(ctx).WithError(err).Warn("Failed to publish activation event") + } } - if err == nil { - as.deviceLastSeenPool.Publish(ctx, lastSeenAtInfo{ids: ids, lastSeenAt: uplink.ReceivedAt}) + // Publish last seen event. + if err := as.deviceLastSeenPool.Publish(ctx, lastSeenAtInfo{ + ids: info.ids, + lastSeenAt: info.uplink.ReceivedAt, + }); err != nil { + log.FromContext(ctx).WithError(err).Warn("Failed to publish last seen event") } return nil } -func (as *ApplicationServer) handleSimulatedUplink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, uplink *ttnpb.ApplicationUplink, link *ttnpb.ApplicationLink) error { +func (as *ApplicationServer) handleSimulatedUplink(ctx context.Context, info uplinkInfo) error { defer trace.StartRegion(ctx, "handle simulated uplink").End() - ctx = log.NewContextWithField(ctx, "session_key_id", uplink.SessionKeyId) - dev, err := as.deviceRegistry.Get(ctx, ids, + ctx = log.NewContextWithField(ctx, "session_key_id", info.uplink.SessionKeyId) + dev, err := as.deviceRegistry.Get(ctx, info.ids, []string{ "formatters", "version_ids", @@ -1141,13 +1202,16 @@ func (as *ApplicationServer) handleSimulatedUplink(ctx context.Context, ids *ttn return err } - if locations, err := as.locationRegistry.Get(ctx, ids); err != nil { + if locations, err := as.locationRegistry.Get(ctx, info.ids); err != nil { log.FromContext(ctx).WithError(err).Warn("Failed to retrieve end device locations") } else { - uplink.Locations = locations + info.uplink.Locations = locations } - return as.decodeUplink(ctx, dev, uplink, link.DefaultFormatters) + if err := as.decodeUplink(ctx, dev, info.uplink, info.link.DefaultFormatters); err != nil { + return err + } + return as.publishNormalizedUplink(ctx, info) } func (as *ApplicationServer) handleDownlinkQueueInvalidated(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, invalid *ttnpb.ApplicationInvalidatedDownlinks, link *ttnpb.ApplicationLink) (pass bool, err error) { @@ -1330,11 +1394,11 @@ func (as *ApplicationServer) GetConfig(ctx context.Context) (*Config, error) { // GetMQTTConfig returns the MQTT frontend configuration based on the context. func (as *ApplicationServer) GetMQTTConfig(ctx context.Context) (*config.MQTT, error) { - config, err := as.GetConfig(ctx) + cfg, err := as.GetConfig(ctx) if err != nil { return nil, err } - return &config.MQTT, nil + return &cfg.MQTT, nil } // RangeUplinks ranges the application uplinks and calls the callback function, until false is returned. diff --git a/pkg/applicationserver/applicationserver_test.go b/pkg/applicationserver/applicationserver_test.go index d9fde14ba3..89bf475901 100644 --- a/pkg/applicationserver/applicationserver_test.go +++ b/pkg/applicationserver/applicationserver_test.go @@ -502,6 +502,9 @@ func TestApplicationServer(t *testing.T) { UplinkMessage: &ttnpb.ApplicationPubSub_Message{ Topic: "up.uplink.message", }, + UplinkNormalized: &ttnpb.ApplicationPubSub_Message{ + Topic: "up.uplink.normalized", + }, JoinAccept: &ttnpb.ApplicationPubSub_Message{ Topic: "up.join.accept", }, @@ -535,17 +538,18 @@ func TestApplicationServer(t *testing.T) { "downlink_ack", "downlink_failed", "downlink_nack", - "downlink_queued", - "downlink_queue_invalidated", - "downlink_sent", "downlink_push", + "downlink_queue_invalidated", + "downlink_queued", "downlink_replace", + "downlink_sent", "format", - "provider", - "service_data", "join_accept", "location_solved", + "provider", + "service_data", "uplink_message", + "uplink_normalized", ), } if _, err := client.Set(ctx, req, creds); err != nil { @@ -644,6 +648,9 @@ func TestApplicationServer(t *testing.T) { UplinkMessage: &ttnpb.ApplicationPubSub_Message{ Topic: "up/uplink/message", }, + UplinkNormalized: &ttnpb.ApplicationPubSub_Message{ + Topic: "up/uplink/normalized", + }, JoinAccept: &ttnpb.ApplicationPubSub_Message{ Topic: "up/join/accept", }, @@ -677,17 +684,18 @@ func TestApplicationServer(t *testing.T) { "downlink_ack", "downlink_failed", "downlink_nack", - "downlink_queued", - "downlink_queue_invalidated", - "downlink_sent", "downlink_push", + "downlink_queue_invalidated", + "downlink_queued", "downlink_replace", + "downlink_sent", "format", - "provider", - "service_data", "join_accept", "location_solved", + "provider", + "service_data", "uplink_message", + "uplink_normalized", ), } if _, err := client.Set(ctx, req, creds); err != nil { @@ -781,31 +789,33 @@ func TestApplicationServer(t *testing.T) { client := ttnpb.NewApplicationWebhookRegistryClient(as.LoopbackConn()) req := &ttnpb.SetApplicationWebhookRequest{ Webhook: &ttnpb.ApplicationWebhook{ - Ids: registeredApplicationWebhookID, - BaseUrl: webhookTarget.URL, - Format: "json", - UplinkMessage: &ttnpb.ApplicationWebhook_Message{Path: ""}, - JoinAccept: &ttnpb.ApplicationWebhook_Message{Path: ""}, - DownlinkAck: &ttnpb.ApplicationWebhook_Message{Path: ""}, - DownlinkNack: &ttnpb.ApplicationWebhook_Message{Path: ""}, - DownlinkQueued: &ttnpb.ApplicationWebhook_Message{Path: ""}, - DownlinkSent: &ttnpb.ApplicationWebhook_Message{Path: ""}, - DownlinkFailed: &ttnpb.ApplicationWebhook_Message{Path: ""}, - LocationSolved: &ttnpb.ApplicationWebhook_Message{Path: ""}, - ServiceData: &ttnpb.ApplicationWebhook_Message{Path: ""}, + Ids: registeredApplicationWebhookID, + BaseUrl: webhookTarget.URL, + Format: "json", + UplinkMessage: &ttnpb.ApplicationWebhook_Message{Path: ""}, + UplinkNormalized: &ttnpb.ApplicationWebhook_Message{Path: ""}, + JoinAccept: &ttnpb.ApplicationWebhook_Message{Path: ""}, + DownlinkAck: &ttnpb.ApplicationWebhook_Message{Path: ""}, + DownlinkNack: &ttnpb.ApplicationWebhook_Message{Path: ""}, + DownlinkQueued: &ttnpb.ApplicationWebhook_Message{Path: ""}, + DownlinkSent: &ttnpb.ApplicationWebhook_Message{Path: ""}, + DownlinkFailed: &ttnpb.ApplicationWebhook_Message{Path: ""}, + LocationSolved: &ttnpb.ApplicationWebhook_Message{Path: ""}, + ServiceData: &ttnpb.ApplicationWebhook_Message{Path: ""}, }, FieldMask: ttnpb.FieldMask( "base_url", - "format", - "uplink_message", - "service_data", - "join_accept", "downlink_ack", + "downlink_failed", "downlink_nack", "downlink_queued", "downlink_sent", - "downlink_failed", + "format", + "join_accept", "location_solved", + "service_data", + "uplink_message", + "uplink_normalized", ), } if _, err := client.Set(ctx, req, creds); err != nil { @@ -939,6 +949,7 @@ func TestApplicationServer(t *testing.T) { Up: &ttnpb.ApplicationUp_JoinAccept{ JoinAccept: &ttnpb.ApplicationJoinAccept{ SessionKeyId: []byte{0x11}, + ReceivedAt: ttnpb.ProtoTimePtr(now), }, }, }, @@ -949,6 +960,7 @@ func TestApplicationServer(t *testing.T) { Up: &ttnpb.ApplicationUp_JoinAccept{ JoinAccept: &ttnpb.ApplicationJoinAccept{ SessionKeyId: []byte{0x11}, + ReceivedAt: up.GetJoinAccept().ReceivedAt, }, }, CorrelationIds: up.CorrelationIds, @@ -985,6 +997,7 @@ func TestApplicationServer(t *testing.T) { EncryptedKey: []byte{0x39, 0x11, 0x40, 0x98, 0xa1, 0x5d, 0x6f, 0x92, 0xd7, 0xf0, 0x13, 0x21, 0x5b, 0x5b, 0x41, 0xa8, 0x98, 0x2d, 0xac, 0x59, 0x34, 0x76, 0x36, 0x18}, KekLabel: "test", }, + ReceivedAt: ttnpb.ProtoTimePtr(now), }, }, }, @@ -995,6 +1008,7 @@ func TestApplicationServer(t *testing.T) { Up: &ttnpb.ApplicationUp_JoinAccept{ JoinAccept: &ttnpb.ApplicationJoinAccept{ SessionKeyId: []byte{0x22}, + ReceivedAt: up.GetJoinAccept().ReceivedAt, }, }, CorrelationIds: up.CorrelationIds, @@ -1106,6 +1120,7 @@ func TestApplicationServer(t *testing.T) { FrmPayload: []byte{0xb, 0x8f, 0x94, 0xe6}, }, }, + ReceivedAt: ttnpb.ProtoTimePtr(now), }, }, }, @@ -1116,6 +1131,7 @@ func TestApplicationServer(t *testing.T) { Up: &ttnpb.ApplicationUp_JoinAccept{ JoinAccept: &ttnpb.ApplicationJoinAccept{ SessionKeyId: []byte{0x33}, + ReceivedAt: up.GetJoinAccept().ReceivedAt, }, }, CorrelationIds: up.CorrelationIds, @@ -1499,6 +1515,7 @@ func TestApplicationServer(t *testing.T) { FrmPayload: []byte{0x2f, 0x3f, 0x31, 0x2c}, }, }, + ReceivedAt: ttnpb.ProtoTimePtr(now), }, }, }, @@ -1510,6 +1527,7 @@ func TestApplicationServer(t *testing.T) { JoinAccept: &ttnpb.ApplicationJoinAccept{ SessionKeyId: []byte{0x44}, PendingSession: true, + ReceivedAt: up.GetJoinAccept().ReceivedAt, }, }, CorrelationIds: up.CorrelationIds, @@ -1884,6 +1902,69 @@ func TestApplicationServer(t *testing.T) { a.So(queue, should.Resemble, []*ttnpb.ApplicationDownlink{}) }, }, + { + Name: "RegisteredDevice/UplinkMessage/KnownSession/", + IDs: registeredDevice.Ids, + Message: &ttnpb.ApplicationUp{ + EndDeviceIds: withDevAddr(registeredDevice.Ids, types.DevAddr{0x55, 0x55, 0x55, 0x55}), + Up: &ttnpb.ApplicationUp_UplinkMessage{ + UplinkMessage: &ttnpb.ApplicationUplink{ + RxMetadata: []*ttnpb.RxMetadata{{GatewayIds: &ttnpb.GatewayIdentifiers{GatewayId: "gtw"}}}, + Settings: &ttnpb.TxSettings{DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{}}}}, + SessionKeyId: []byte{0x55}, + FPort: 42, + FCnt: 42, + FrmPayload: []byte{0xd1, 0x43, 0x6a}, + ReceivedAt: ttnpb.ProtoTimePtr(now), + }, + }, + }, + AssertUp: func(t *testing.T, up *ttnpb.ApplicationUp) { + a := assertions.New(t) + a.So(up, should.Resemble, &ttnpb.ApplicationUp{ + EndDeviceIds: withDevAddr(registeredDevice.Ids, types.DevAddr{0x55, 0x55, 0x55, 0x55}), + Up: &ttnpb.ApplicationUp_UplinkMessage{ + UplinkMessage: &ttnpb.ApplicationUplink{ + RxMetadata: []*ttnpb.RxMetadata{{GatewayIds: &ttnpb.GatewayIdentifiers{GatewayId: "gtw"}}}, + Settings: &ttnpb.TxSettings{DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{}}}}, + SessionKeyId: []byte{0x55}, + FPort: 42, + FCnt: 42, + FrmPayload: []byte{0x2a, 0x2a, 0x2a}, + DecodedPayload: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "sum": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 126, // Payload formatter sums the bytes in FRMPayload. + }, + }, + }, + }, + VersionIds: registeredDevice.VersionIds, + ReceivedAt: up.GetUplinkMessage().ReceivedAt, + }, + }, + CorrelationIds: up.CorrelationIds, + ReceivedAt: up.ReceivedAt, + }) + }, + AssertDevice: func(t *testing.T, dev *ttnpb.EndDevice, queue []*ttnpb.ApplicationDownlink) { + a := assertions.New(t) + a.So(dev.Session, should.Resemble, &ttnpb.Session{ + DevAddr: types.DevAddr{0x55, 0x55, 0x55, 0x55}.Bytes(), + Keys: &ttnpb.SessionKeys{ + SessionKeyId: []byte{0x55}, + AppSKey: &ttnpb.KeyEnvelope{ + EncryptedKey: []byte{0x56, 0x15, 0xaa, 0x22, 0xb7, 0x5f, 0xc, 0x24, 0x79, 0x6, 0x84, 0x68, 0x89, 0x0, 0xa6, 0x16, 0x4a, 0x9c, 0xef, 0xdb, 0xbf, 0x61, 0x6f, 0x0}, + KekLabel: "test", + }, + }, + LastAFCntDown: 0, + }) + a.So(dev.PendingSession, should.BeNil) + a.So(queue, should.Resemble, []*ttnpb.ApplicationDownlink{}) + }, + }, { Name: "UnregisteredDevice/JoinAccept", IDs: unregisteredDeviceID, @@ -1898,6 +1979,7 @@ func TestApplicationServer(t *testing.T) { EncryptedKey: []byte{0x56, 0x15, 0xaa, 0x22, 0xb7, 0x5f, 0xc, 0x24, 0x79, 0x6, 0x84, 0x68, 0x89, 0x0, 0xa6, 0x16, 0x4a, 0x9c, 0xef, 0xdb, 0xbf, 0x61, 0x6f, 0x0}, KekLabel: "test", }, + ReceivedAt: ttnpb.ProtoTimePtr(now), }, }, }, @@ -2430,6 +2512,7 @@ func TestSkipPayloadCrypto(t *testing.T) { EncryptedKey: []byte{0x39, 0x11, 0x40, 0x98, 0xa1, 0x5d, 0x6f, 0x92, 0xd7, 0xf0, 0x13, 0x21, 0x5b, 0x5b, 0x41, 0xa8, 0x98, 0x2d, 0xac, 0x59, 0x34, 0x76, 0x36, 0x18}, KekLabel: kekLabel, }, + ReceivedAt: ttnpb.ProtoTimePtr(now), }, }, }, @@ -2447,6 +2530,7 @@ func TestSkipPayloadCrypto(t *testing.T) { EncryptedKey: []byte{0x39, 0x11, 0x40, 0x98, 0xa1, 0x5d, 0x6f, 0x92, 0xd7, 0xf0, 0x13, 0x21, 0x5b, 0x5b, 0x41, 0xa8, 0x98, 0x2d, 0xac, 0x59, 0x34, 0x76, 0x36, 0x18}, KekLabel: kekLabel, }, + ReceivedAt: up.GetJoinAccept().ReceivedAt, }, }, CorrelationIds: up.CorrelationIds, @@ -2458,6 +2542,7 @@ func TestSkipPayloadCrypto(t *testing.T) { Up: &ttnpb.ApplicationUp_JoinAccept{ JoinAccept: &ttnpb.ApplicationJoinAccept{ SessionKeyId: []byte{0x22}, + ReceivedAt: up.GetJoinAccept().ReceivedAt, }, }, CorrelationIds: up.CorrelationIds, @@ -2894,6 +2979,188 @@ func TestLocationFromPayload(t *testing.T) { } } +func TestUplinkNormalized(t *testing.T) { + a, ctx := test.New(t) + + registeredApplicationID := ttnpb.ApplicationIdentifiers{ApplicationId: "foo-app"} + + // This device gets registered in the device registry of the Application Server. + registeredDevice := &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: ®isteredApplicationID, + DeviceId: "foo-device", + JoinEui: types.EUI64{0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + DevEui: types.EUI64{0x42, 0x42, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}.Bytes(), + }, + Session: &ttnpb.Session{ + DevAddr: types.DevAddr{0x11, 0x11, 0x11, 0x11}.Bytes(), + Keys: &ttnpb.SessionKeys{ + SessionKeyId: []byte{0x11}, + AppSKey: &ttnpb.KeyEnvelope{ + Key: types.AES128Key{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}.Bytes(), //nolint:lll + }, + }, + }, + Formatters: &ttnpb.MessagePayloadFormatters{ + UpFormatter: ttnpb.PayloadFormatter_FORMATTER_JAVASCRIPT, + UpFormatterParameter: `function decodeUplink(input) { + return { + data: { + air: { + temperature: 21.5, + } + } + }; + }`, + }, + } + + is, isAddr, closeIS := mockis.New(ctx) + defer closeIS() + is.EndDeviceRegistry().Add(ctx, registeredDevice) + + devsRedisClient, devsFlush := test.NewRedis(ctx, "applicationserver_test", "devices") + defer devsFlush() + defer devsRedisClient.Close() + deviceRegistry := &redis.DeviceRegistry{Redis: devsRedisClient, LockTTL: test.Delay << 10} + if err := deviceRegistry.Init(ctx); !a.So(err, should.BeNil) { + t.FailNow() + } + _, err := deviceRegistry.Set(ctx, registeredDevice.Ids, nil, func(ed *ttnpb.EndDevice) (*ttnpb.EndDevice, []string, error) { + return registeredDevice, []string{"ids", "session", "formatters"}, nil + }) + if err != nil { + t.Fatalf("Failed to set device in registry: %s", err) + } + + linksRedisClient, linksFlush := test.NewRedis(ctx, "applicationserver_test", "links") + defer linksFlush() + defer linksRedisClient.Close() + linkRegistry := &redis.LinkRegistry{Redis: linksRedisClient, LockTTL: test.Delay << 10} + if err := linkRegistry.Init(ctx); !a.So(err, should.BeNil) { + t.FailNow() + } + _, err = linkRegistry.Set(ctx, ®isteredApplicationID, nil, func(_ *ttnpb.ApplicationLink) (*ttnpb.ApplicationLink, []string, error) { + return &ttnpb.ApplicationLink{}, nil, nil + }) + if err != nil { + t.Fatalf("Failed to set link in registry: %s", err) + } + + distribRedisClient, distribFlush := test.NewRedis(ctx, "applicationserver_test", "traffic") + defer distribFlush() + defer distribRedisClient.Close() + distribPubSub := distribredis.PubSub{Redis: distribRedisClient} + + applicationUpsRedisClient, applicationUpsFlush := test.NewRedis(ctx, "applicationserver_test", "applicationups") + defer applicationUpsFlush() + defer applicationUpsRedisClient.Close() + applicationUpsRegistry := &redis.ApplicationUplinkRegistry{ + Redis: applicationUpsRedisClient, + Limit: 16, + } + + c := componenttest.NewComponent(t, &component.Config{ + ServiceBase: config.ServiceBase{ + GRPC: config.GRPC{ + Listen: ":9189", + AllowInsecureForCredentials: true, + }, + Cluster: cluster.Config{ + IdentityServer: isAddr, + }, + HTTP: config.HTTP{ + Listen: ":8100", + }, + }, + }) + config := &applicationserver.Config{ + Devices: deviceRegistry, + Links: linkRegistry, + UplinkStorage: applicationserver.UplinkStorageConfig{ + Registry: applicationUpsRegistry, + Limit: 16, + }, + Distribution: applicationserver.DistributionConfig{ + Global: applicationserver.GlobalDistributorConfig{ + PubSub: distribPubSub, + }, + }, + EndDeviceMetadataStorage: applicationserver.EndDeviceMetadataStorageConfig{ + Location: applicationserver.EndDeviceLocationStorageConfig{ + Registry: metadata.NewClusterEndDeviceLocationRegistry(c, (1<<4)*Timeout), + }, + }, + } + as, err := applicationserver.New(c, config) + if !a.So(err, should.BeNil) { + t.FailNow() + } + + roles := as.Roles() + a.So(len(roles), should.Equal, 1) + a.So(roles[0], should.Equal, ttnpb.ClusterRole_APPLICATION_SERVER) + + componenttest.StartComponent(t, c) + defer c.Close() + + mustHavePeer(ctx, c, ttnpb.ClusterRole_ENTITY_REGISTRY) + + sub, err := as.Subscribe(ctx, "test", nil, false) + a.So(err, should.BeNil) + + now := time.Now().UTC() + err = as.Publish(ctx, &ttnpb.ApplicationUp{ + EndDeviceIds: registeredDevice.Ids, + Up: &ttnpb.ApplicationUp_UplinkMessage{ + UplinkMessage: &ttnpb.ApplicationUplink{ + RxMetadata: []*ttnpb.RxMetadata{{GatewayIds: &ttnpb.GatewayIdentifiers{GatewayId: "gtw"}}}, + Settings: &ttnpb.TxSettings{DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{}}}}, + SessionKeyId: []byte{0x11}, + FPort: 11, + FCnt: 11, + FrmPayload: []byte{0x11}, + ReceivedAt: ttnpb.ProtoTimePtr(now), + }, + }, + }) + a.So(err, should.BeNil) + + // The uplink message and the normalized payload message may come out of order. + // Expect exactly two messages. + var normalized *ttnpb.ApplicationUplinkNormalized + for i := 0; i < 2; i++ { + select { + case msg := <-sub.Up(): + if n := msg.GetUplinkNormalized(); n != nil { + normalized = n + } + case <-time.After(Timeout): + t.Fatalf("Expected upstream message %d timed out", i) + } + } + if normalized == nil { + t.Fatalf("Expected uplink normalized message") + } + a.So(normalized.NormalizedPayload, should.Resemble, &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 21.5, + }, + }, + }, + }, + }, + }, + }, + }) +} + func TestApplicationServerCleanup(t *testing.T) { a, ctx := test.New(t) diff --git a/pkg/applicationserver/grpc.go b/pkg/applicationserver/grpc.go index 3d340fba38..3fb51acf7e 100644 --- a/pkg/applicationserver/grpc.go +++ b/pkg/applicationserver/grpc.go @@ -17,6 +17,7 @@ package applicationserver import ( "context" "fmt" + "time" pbtypes "github.com/gogo/protobuf/types" clusterauth "go.thethings.network/lorawan-stack/v3/pkg/auth/cluster" @@ -125,6 +126,7 @@ func (as *ApplicationServer) GetConfiguration(ctx context.Context, _ *ttnpb.GetA // HandleUplink implements ttnpb.NsAsServer. func (as *ApplicationServer) HandleUplink(ctx context.Context, req *ttnpb.NsAsHandleUplinkRequest) (*pbtypes.Empty, error) { + now := time.Now() if err := clusterauth.Authorized(ctx); err != nil { return nil, err } @@ -136,6 +138,7 @@ func (as *ApplicationServer) HandleUplink(ctx context.Context, req *ttnpb.NsAsHa return nil, err } for _, up := range req.ApplicationUps { + up.ReceivedAt = ttnpb.ProtoTimePtr(now) if err := as.processUp(ctx, up, link); err != nil { return nil, err } diff --git a/pkg/applicationserver/io/grpc/grpc_test.go b/pkg/applicationserver/io/grpc/grpc_test.go index eb0f9e6bd1..8ec7a5ce64 100644 --- a/pkg/applicationserver/io/grpc/grpc_test.go +++ b/pkg/applicationserver/io/grpc/grpc_test.go @@ -53,6 +53,7 @@ var ( ) func TestAuthentication(t *testing.T) { + t.Parallel() ctx := log.NewContext(test.Context(), test.GetLogger(t)) is, isAddr, closeIS := mockis.New(ctx) @@ -80,6 +81,7 @@ func TestAuthentication(t *testing.T) { client := ttnpb.NewAppAsClient(c.LoopbackConn()) + //nolint:paralleltest for _, tc := range []struct { ID *ttnpb.ApplicationIdentifiers Key string @@ -138,6 +140,7 @@ type erroredApplicationUp struct { } func TestTraffic(t *testing.T) { + t.Parallel() ctx := log.NewContext(test.Context(), test.GetLogger(t)) ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -197,6 +200,7 @@ func TestTraffic(t *testing.T) { t.Fatal("Subscription timeout") } + //nolint:paralleltest t.Run("Upstream", func(t *testing.T) { a := assertions.New(t) @@ -224,6 +228,7 @@ func TestTraffic(t *testing.T) { } }) + //nolint:paralleltest t.Run("Downstream", func(t *testing.T) { a := assertions.New(t) ids := ttnpb.EndDeviceIdentifiers{ @@ -365,6 +370,7 @@ func (p mockMQTTConfigProvider) GetMQTTConfig(context.Context) (*config.MQTT, er } func TestMQTTConfig(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) @@ -416,6 +422,7 @@ func TestMQTTConfig(t *testing.T) { } func TestSimulateUplink(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) @@ -471,6 +478,7 @@ func TestSimulateUplink(t *testing.T) { }() <-as.Subscriptions() + //nolint:paralleltest for _, tc := range []struct { name string up *ttnpb.ApplicationUp @@ -537,6 +545,7 @@ func TestSimulateUplink(t *testing.T) { } func TestMessageProcessors(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) @@ -611,8 +620,12 @@ func TestMessageProcessors(t *testing.T) { Uplink: &ttnpb.ApplicationUplink{ FrmPayload: []byte{1, 0, 255}, RxMetadata: []*ttnpb.RxMetadata{{GatewayIds: &ttnpb.GatewayIdentifiers{GatewayId: "gtw"}}}, - Settings: &ttnpb.TxSettings{DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{}}}}, - FPort: 1, + Settings: &ttnpb.TxSettings{ + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{}}, + }, + }, + FPort: 1, }, Formatter: ttnpb.PayloadFormatter_FORMATTER_CAYENNELPP, }, creds) diff --git a/pkg/applicationserver/io/mqtt/format.go b/pkg/applicationserver/io/mqtt/format.go index 19dfbabea0..37c644eb5d 100644 --- a/pkg/applicationserver/io/mqtt/format.go +++ b/pkg/applicationserver/io/mqtt/format.go @@ -33,7 +33,9 @@ func TopicParts(up *io.ContextualApplicationUp, layout topics.Layout) []string { var f func(string, string) []string switch up.Up.(type) { case *ttnpb.ApplicationUp_UplinkMessage: - f = layout.UplinkTopic + f = layout.UplinkMessageTopic + case *ttnpb.ApplicationUp_UplinkNormalized: + f = layout.UplinkNormalizedTopic case *ttnpb.ApplicationUp_JoinAccept: f = layout.JoinAcceptTopic case *ttnpb.ApplicationUp_DownlinkAck: diff --git a/pkg/applicationserver/io/mqtt/mqtt.go b/pkg/applicationserver/io/mqtt/mqtt.go index 5bf597e69d..4482171125 100644 --- a/pkg/applicationserver/io/mqtt/mqtt.go +++ b/pkg/applicationserver/io/mqtt/mqtt.go @@ -99,9 +99,10 @@ func setupConnection(ctx context.Context, mqttConn mqttnet.Conn, format Format, logger.WithError(err).Warn("Failed to marshal upstream message") continue } - logger.Debug("Publish upstream message") + topicName := topic.Join(topicParts) + logger.WithField("topic", topicName).Debug("Publish upstream message") session.Publish(&packet.PublishPacket{ - TopicName: topic.Join(topicParts), + TopicName: topicName, TopicParts: topicParts, QoS: qosUpstream, Message: buf, @@ -183,7 +184,8 @@ func (c *connection) Connect(ctx context.Context, info *auth.Info) (_ context.Co } if err := rights.RequireApplication(ctx, ids, ttnpb.Right_RIGHT_APPLICATION_TRAFFIC_READ); err == nil { access.reads = append(access.reads, - c.format.UplinkTopic(uid, topic.PartWildcard), + c.format.UplinkMessageTopic(uid, topic.PartWildcard), + c.format.UplinkNormalizedTopic(uid, topic.PartWildcard), c.format.JoinAcceptTopic(uid, topic.PartWildcard), c.format.DownlinkAckTopic(uid, topic.PartWildcard), c.format.DownlinkNackTopic(uid, topic.PartWildcard), diff --git a/pkg/applicationserver/io/mqtt/mqtt_test.go b/pkg/applicationserver/io/mqtt/mqtt_test.go index acd2c26982..424641a8c1 100644 --- a/pkg/applicationserver/io/mqtt/mqtt_test.go +++ b/pkg/applicationserver/io/mqtt/mqtt_test.go @@ -22,6 +22,7 @@ import ( "time" mqtt "github.com/eclipse/paho.mqtt.golang" + pbtypes "github.com/gogo/protobuf/types" "github.com/smartystreets/assertions" "go.thethings.network/lorawan-stack/v3/pkg/applicationserver/io" "go.thethings.network/lorawan-stack/v3/pkg/applicationserver/io/mock" @@ -210,6 +211,38 @@ func TestTraffic(t *testing.T) { }, OK: true, }, + { + Topic: fmt.Sprintf("v3/%v/devices/%v/up/normalized", unique.ID(ctx, registeredDeviceID.ApplicationIds), registeredDeviceID.DeviceId), + Message: &ttnpb.ApplicationUp{ + EndDeviceIds: registeredDeviceID, + Up: &ttnpb.ApplicationUp_UplinkNormalized{ + UplinkNormalized: &ttnpb.ApplicationUplinkNormalized{ + SessionKeyId: []byte{0x11}, + FPort: 42, + FCnt: 42, + FrmPayload: []byte{0x1, 0x2, 0x3}, + NormalizedPayload: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 21.5, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + OK: true, + }, { Topic: fmt.Sprintf("v3/%v/devices/%v/join", unique.ID(ctx, registeredDeviceID.ApplicationIds), registeredDeviceID.DeviceId), Message: &ttnpb.ApplicationUp{ diff --git a/pkg/applicationserver/io/mqtt/topics/layout.go b/pkg/applicationserver/io/mqtt/topics/layout.go index 49ddd0569a..915f29886a 100644 --- a/pkg/applicationserver/io/mqtt/topics/layout.go +++ b/pkg/applicationserver/io/mqtt/topics/layout.go @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package topics implements MQTT topic layouts. package topics // Layout represents an MQTT topic layout. type Layout interface { AcceptedTopic(applicationUID string, requested []string) (accepted []string, ok bool) - UplinkTopic(applicationUID, deviceID string) []string + UplinkMessageTopic(applicationUID, deviceID string) []string + UplinkNormalizedTopic(applicationUID, deviceID string) []string JoinAcceptTopic(applicationUID, deviceID string) []string DownlinkAckTopic(applicationUID, deviceID string) []string DownlinkNackTopic(applicationUID, deviceID string) []string diff --git a/pkg/applicationserver/io/mqtt/topics/v3.go b/pkg/applicationserver/io/mqtt/topics/v3.go index c06e338073..d68c99440f 100644 --- a/pkg/applicationserver/io/mqtt/topics/v3.go +++ b/pkg/applicationserver/io/mqtt/topics/v3.go @@ -44,10 +44,14 @@ func (v3) AcceptedTopic(applicationUID string, requested []string) ([]string, bo return nil, false } -func (v3) UplinkTopic(applicationUID, deviceID string) []string { +func (v3) UplinkMessageTopic(applicationUID, deviceID string) []string { return []string{topicV3, applicationUID, "devices", deviceID, "up"} } +func (v3) UplinkNormalizedTopic(applicationUID, deviceID string) []string { + return []string{topicV3, applicationUID, "devices", deviceID, "up", "normalized"} +} + func (v3) JoinAcceptTopic(applicationUID, deviceID string) []string { return []string{topicV3, applicationUID, "devices", deviceID, "join"} } diff --git a/pkg/applicationserver/io/mqtt/topics/v3_test.go b/pkg/applicationserver/io/mqtt/topics/v3_test.go index 5e53d96c23..bdf336eada 100644 --- a/pkg/applicationserver/io/mqtt/topics/v3_test.go +++ b/pkg/applicationserver/io/mqtt/topics/v3_test.go @@ -30,6 +30,7 @@ import ( ) func TestV3AcceptedTopic(t *testing.T) { + t.Parallel() uid := unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ApplicationId: "foo-app"}) for i, tc := range []struct { Requested, @@ -63,7 +64,9 @@ func TestV3AcceptedTopic(t *testing.T) { OK: true, }, } { + tc := tc t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() a := assertions.New(t) actual, ok := topics.Default.AcceptedTopic(uid, topic.Split(tc.Requested)) if !a.So(ok, should.Equal, tc.OK) { @@ -75,6 +78,7 @@ func TestV3AcceptedTopic(t *testing.T) { } func TestV3Topics(t *testing.T) { + t.Parallel() appUID := unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ApplicationId: "foo-app"}) devID := "foo-device" @@ -83,9 +87,13 @@ func TestV3Topics(t *testing.T) { Expected string }{ { - Fn: topics.Default.UplinkTopic, + Fn: topics.Default.UplinkMessageTopic, Expected: fmt.Sprintf("v3/%s/devices/%s/up", appUID, devID), }, + { + Fn: topics.Default.UplinkNormalizedTopic, + Expected: fmt.Sprintf("v3/%s/devices/%s/up/normalized", appUID, devID), + }, { Fn: topics.Default.JoinAcceptTopic, Expected: fmt.Sprintf("v3/%s/devices/%s/join", appUID, devID), @@ -131,7 +139,9 @@ func TestV3Topics(t *testing.T) { Expected: fmt.Sprintf("v3/%s/devices/%s/down/replace", appUID, devID), }, } { + tc := tc t.Run(tc.Expected, func(t *testing.T) { + t.Parallel() actual := strings.Join(tc.Fn(appUID, devID), "/") assertions.New(t).So(actual, should.Equal, tc.Expected) }) diff --git a/pkg/applicationserver/io/pubsub/provider/connection.go b/pkg/applicationserver/io/pubsub/provider/connection.go index 7f684e51c7..d32503b7e9 100644 --- a/pkg/applicationserver/io/pubsub/provider/connection.go +++ b/pkg/applicationserver/io/pubsub/provider/connection.go @@ -42,6 +42,7 @@ func (ds *DownlinkSubscriptions) Shutdown(ctx context.Context) error { // UplinkTopics contains the topics for the uplink messages. type UplinkTopics struct { UplinkMessage *pubsub.Topic + UplinkNormalized *pubsub.Topic JoinAccept *pubsub.Topic DownlinkAck *pubsub.Topic DownlinkNack *pubsub.Topic @@ -57,6 +58,7 @@ type UplinkTopics struct { func (ut *UplinkTopics) Shutdown(ctx context.Context) error { return shutdown(ctx, ut.UplinkMessage, + ut.UplinkNormalized, ut.JoinAccept, ut.DownlinkAck, ut.DownlinkNack, @@ -75,7 +77,7 @@ type Shutdowner interface { } // ProviderConnection is an interface that represents a provider specific connection. -type ProviderConnection interface { +type ProviderConnection interface { //nolint:revive Shutdowner } diff --git a/pkg/applicationserver/io/pubsub/provider/mock/provider.go b/pkg/applicationserver/io/pubsub/provider/mock/provider.go index 73b6fa5f88..a10aefa06f 100644 --- a/pkg/applicationserver/io/pubsub/provider/mock/provider.go +++ b/pkg/applicationserver/io/pubsub/provider/mock/provider.go @@ -48,6 +48,7 @@ type Connection struct { Replace *pubsub.Topic UplinkMessage *pubsub.Subscription + UplinkNormalized *pubsub.Subscription JoinAccept *pubsub.Subscription DownlinkAck *pubsub.Subscription DownlinkNack *pubsub.Subscription @@ -80,6 +81,7 @@ func (c *Connection) Shutdown(ctx context.Context) (err error) { c.Replace, c.UplinkMessage, + c.UplinkNormalized, c.JoinAccept, c.DownlinkAck, c.DownlinkNack, @@ -132,6 +134,10 @@ func (i *Impl) OpenConnection(ctx context.Context, target provider.Target, enabl topic: &pc.Topics.UplinkMessage, subscription: &conn.UplinkMessage, }, + { + topic: &pc.Topics.UplinkNormalized, + subscription: &conn.UplinkNormalized, + }, { topic: &pc.Topics.JoinAccept, subscription: &conn.JoinAccept, diff --git a/pkg/applicationserver/io/pubsub/provider/nats/provider.go b/pkg/applicationserver/io/pubsub/provider/nats/provider.go index 133297afc3..536d164519 100644 --- a/pkg/applicationserver/io/pubsub/provider/nats/provider.go +++ b/pkg/applicationserver/io/pubsub/provider/nats/provider.go @@ -64,6 +64,10 @@ func (impl) OpenConnection(ctx context.Context, target provider.Target, enabler topic: &pc.Topics.UplinkMessage, message: target.GetUplinkMessage(), }, + { + topic: &pc.Topics.UplinkNormalized, + message: target.GetUplinkNormalized(), + }, { topic: &pc.Topics.JoinAccept, message: target.GetJoinAccept(), diff --git a/pkg/applicationserver/io/pubsub/provider/nats/provider_test.go b/pkg/applicationserver/io/pubsub/provider/nats/provider_test.go index 6b991e3ec0..63d6d75432 100644 --- a/pkg/applicationserver/io/pubsub/provider/nats/provider_test.go +++ b/pkg/applicationserver/io/pubsub/provider/nats/provider_test.go @@ -77,6 +77,9 @@ func TestOpenConnection(t *testing.T) { UplinkMessage: &ttnpb.ApplicationPubSub_Message{ Topic: "uplink.message", }, + UplinkNormalized: &ttnpb.ApplicationPubSub_Message{ + Topic: "uplink.normalized", + }, JoinAccept: &ttnpb.ApplicationPubSub_Message{ Topic: "join.accept", }, @@ -204,6 +207,11 @@ func TestOpenConnection(t *testing.T) { subject: "app1.ps1.uplink.message", topic: conn.Topics.UplinkMessage, }, + { + name: "ValidNormalizedUplink", + subject: "app1.ps1.uplink.normalized", + topic: conn.Topics.UplinkNormalized, + }, { name: "ValidJoinAccept", subject: "app1.ps1.join.accept", diff --git a/pkg/applicationserver/io/pubsub/provider/provider.go b/pkg/applicationserver/io/pubsub/provider/provider.go index 64a908f4f9..41a8d82af4 100644 --- a/pkg/applicationserver/io/pubsub/provider/provider.go +++ b/pkg/applicationserver/io/pubsub/provider/provider.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package provider implements pub/sub provider interfaces and registration. package provider import ( @@ -27,6 +28,7 @@ import ( type Topics interface { GetBaseTopic() string GetUplinkMessage() *ttnpb.ApplicationPubSub_Message + GetUplinkNormalized() *ttnpb.ApplicationPubSub_Message GetJoinAccept() *ttnpb.ApplicationPubSub_Message GetDownlinkAck() *ttnpb.ApplicationPubSub_Message GetDownlinkNack() *ttnpb.ApplicationPubSub_Message @@ -57,8 +59,14 @@ type Provider interface { } var ( - errNotImplemented = errors.DefineUnimplemented("provider_not_implemented", "provider `{provider_id}` is not implemented") - errAlreadyRegistered = errors.DefineAlreadyExists("provider_already_registered", "provider `{provider_id}` already registered") + errNotImplemented = errors.DefineUnimplemented( + "provider_not_implemented", + "provider `{provider_id}` is not implemented", + ) + errAlreadyRegistered = errors.DefineAlreadyExists( + "provider_already_registered", + "provider `{provider_id}` already registered", + ) providers = map[reflect.Type]Provider{} ) diff --git a/pkg/applicationserver/io/pubsub/pubsub.go b/pkg/applicationserver/io/pubsub/pubsub.go index a6db1f4471..751c0807a3 100644 --- a/pkg/applicationserver/io/pubsub/pubsub.go +++ b/pkg/applicationserver/io/pubsub/pubsub.go @@ -47,7 +47,12 @@ type PubSub struct { } // New creates a new pusub frontend. -func New(c *component.Component, server io.Server, registry Registry, providerStatuses ProviderStatuses) (*PubSub, error) { +func New( + c *component.Component, + server io.Server, + registry Registry, + providerStatuses ProviderStatuses, +) (*PubSub, error) { ctx := log.NewContextWithField(c.Context(), "namespace", "applicationserver/io/pubsub") ps := &PubSub{ Component: c, @@ -130,6 +135,8 @@ func (i *integration) handleUp(ctx context.Context) { switch up.ApplicationUp.Up.(type) { case *ttnpb.ApplicationUp_UplinkMessage: topic = i.conn.Topics.UplinkMessage + case *ttnpb.ApplicationUp_UplinkNormalized: + topic = i.conn.Topics.UplinkNormalized case *ttnpb.ApplicationUp_JoinAccept: topic = i.conn.Topics.JoinAccept case *ttnpb.ApplicationUp_DownlinkAck: @@ -170,7 +177,11 @@ func (i *integration) handleUp(ctx context.Context) { } } -func (i *integration) handleDown(ctx context.Context, op func(io.Server, context.Context, *ttnpb.EndDeviceIdentifiers, []*ttnpb.ApplicationDownlink) error, subscription *pubsub.Subscription) { +func (i *integration) handleDown( + ctx context.Context, + op func(io.Server, context.Context, *ttnpb.EndDeviceIdentifiers, []*ttnpb.ApplicationDownlink) error, + subscription *pubsub.Subscription, +) { logger := log.FromContext(ctx) for { select { @@ -268,14 +279,14 @@ func (ps *PubSub) start(ctx context.Context, pb *ttnpb.ApplicationPubSub) (err e } }() - provider, err := provider.GetProvider(pb) + p, err := provider.GetProvider(pb) if err != nil { return err } if err := ps.providerStatuses.Enabled(ctx, pb.GetProvider()); err != nil { return err } - i.conn, err = provider.OpenConnection(ctx, pb, ps.providerStatuses) + i.conn, err = p.OpenConnection(ctx, pb, ps.providerStatuses) if err != nil { return err } diff --git a/pkg/applicationserver/io/pubsub/pubsub_test.go b/pkg/applicationserver/io/pubsub/pubsub_test.go index 9ca729f1a0..70f612bb6c 100644 --- a/pkg/applicationserver/io/pubsub/pubsub_test.go +++ b/pkg/applicationserver/io/pubsub/pubsub_test.go @@ -19,7 +19,7 @@ import ( "testing" "time" - "github.com/gogo/protobuf/types" + pbtypes "github.com/gogo/protobuf/types" "go.thethings.network/lorawan-stack/v3/pkg/applicationserver/io/formatters" mock_server "go.thethings.network/lorawan-stack/v3/pkg/applicationserver/io/mock" "go.thethings.network/lorawan-stack/v3/pkg/applicationserver/io/pubsub" @@ -41,6 +41,7 @@ type messageWithError struct { } func TestPubSub(t *testing.T) { + t.Parallel() a, ctx := test.New(t) redisClient, flush := test.NewRedis(ctx, "pubsub_test") @@ -79,6 +80,9 @@ func TestPubSub(t *testing.T) { UplinkMessage: &ttnpb.ApplicationPubSub_Message{ Topic: "uplink.message", }, + UplinkNormalized: &ttnpb.ApplicationPubSub_Message{ + Topic: "uplink.normalized", + }, JoinAccept: &ttnpb.ApplicationPubSub_Message{ Topic: "join.accept", }, @@ -122,6 +126,7 @@ func TestPubSub(t *testing.T) { "provider", "join_accept", "location_solved", + "uplink_normalized", "uplink_message", "service_data", } @@ -158,6 +163,7 @@ func TestPubSub(t *testing.T) { sub := <-io.Subscriptions() conn := <-mockImpl.OpenConnectionCh + //nolint:paralleltest t.Run("Upstream", func(t *testing.T) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -182,6 +188,38 @@ func TestPubSub(t *testing.T) { }, Subscription: conn.UplinkMessage, }, + { + Name: "UplinkNormalized", + Message: &ttnpb.ApplicationUp{ + EndDeviceIds: registeredDeviceID, + Up: &ttnpb.ApplicationUp_UplinkNormalized{ + UplinkNormalized: &ttnpb.ApplicationUplinkNormalized{ + SessionKeyId: []byte{0x11}, + FPort: 42, + FCnt: 42, + FrmPayload: []byte{0x1, 0x2, 0x3}, + NormalizedPayload: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 21.5, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Subscription: conn.UplinkNormalized, + }, { Name: "JoinAccept", Message: &ttnpb.ApplicationUp{ @@ -318,10 +356,10 @@ func TestPubSub(t *testing.T) { EndDeviceIds: registeredDeviceID, Up: &ttnpb.ApplicationUp_ServiceData{ ServiceData: &ttnpb.ApplicationServiceData{ - Data: &types.Struct{ - Fields: map[string]*types.Value{ + Data: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ "battery": { - Kind: &types.Value_NumberValue{ + Kind: &pbtypes.Value_NumberValue{ NumberValue: 42.0, }, }, @@ -370,6 +408,7 @@ func TestPubSub(t *testing.T) { } }) + //nolint:paralleltest t.Run("Downstream", func(t *testing.T) { for _, tc := range []struct { Name string diff --git a/pkg/applicationserver/io/web/templates.go b/pkg/applicationserver/io/web/templates.go index 9bf7925c19..bb63c0e857 100644 --- a/pkg/applicationserver/io/web/templates.go +++ b/pkg/applicationserver/io/web/templates.go @@ -224,6 +224,7 @@ func (f webhookTemplateField) toPB() *ttnpb.ApplicationWebhookTemplateField { type webhookTemplatePaths struct { UplinkMessage *string `yaml:"uplink-message,omitempty"` + UplinkNormalized *string `yaml:"uplink-normalized,omitempty"` JoinAccept *string `yaml:"join-accept,omitempty"` DownlinkAck *string `yaml:"downlink-ack,omitempty"` DownlinkNack *string `yaml:"downlink-nack,omitempty"` @@ -288,6 +289,7 @@ func (t webhookTemplate) toPB() *ttnpb.ApplicationWebhookTemplate { Fields: t.pbFields(), CreateDownlinkApiKey: t.CreateDownlinkAPIKey, UplinkMessage: t.pathToMessage(t.Paths.UplinkMessage), + UplinkNormalized: t.pathToMessage(t.Paths.UplinkNormalized), JoinAccept: t.pathToMessage(t.Paths.JoinAccept), DownlinkAck: t.pathToMessage(t.Paths.DownlinkAck), DownlinkNack: t.pathToMessage(t.Paths.DownlinkNack), diff --git a/pkg/applicationserver/io/web/webhooks.go b/pkg/applicationserver/io/web/webhooks.go index 318e2e00ff..0af9b0cf8a 100644 --- a/pkg/applicationserver/io/web/webhooks.go +++ b/pkg/applicationserver/io/web/webhooks.go @@ -83,7 +83,7 @@ func (s *HTTPClientSink) Process(req *http.Request) error { return errRequest.WithCause(err).WithDetails(createRequestErrorDetails(req, res)...) } defer res.Body.Close() - defer stdio.Copy(stdio.Discard, res.Body) + defer stdio.Copy(stdio.Discard, res.Body) //nolint:errcheck if res.StatusCode >= 200 && res.StatusCode <= 299 { return nil } @@ -125,10 +125,7 @@ func NewPooledSink(ctx context.Context, c workerpool.Component, sink Sink, worke // Process sends the request to the workers. // This method returns immediately. An error is returned when the workers are too busy. func (s *pooledSink) Process(req *http.Request) error { - if err := s.pool.Publish(req.Context(), req); err != nil { - return err - } - return nil + return s.pool.Publish(req.Context(), req) } // Webhooks is an interface for registering incoming webhooks for downlink and creating a subscription to outgoing @@ -147,7 +144,13 @@ type webhooks struct { } // NewWebhooks returns a new Webhooks. -func NewWebhooks(ctx context.Context, server io.Server, registry WebhookRegistry, target Sink, downlinks DownlinksConfig) (Webhooks, error) { +func NewWebhooks( + ctx context.Context, + server io.Server, + registry WebhookRegistry, + target Sink, + downlinks DownlinksConfig, +) (Webhooks, error) { ctx = log.NewContextWithField(ctx, "namespace", namespace) w := &webhooks{ ctx: ctx, @@ -174,7 +177,9 @@ func (w *webhooks) Registry() WebhookRegistry { return w.registry } // RegisterRoutes registers the webhooks to the web server to handle downlink requests. func (w *webhooks) RegisterRoutes(server *ttnweb.Server) { - router := server.Prefix(ttnpb.HTTPAPIPrefix + "/as/applications/{application_id}/webhooks/{webhook_id}/devices/{device_id}/down").Subrouter() + router := server.Prefix( + ttnpb.HTTPAPIPrefix + "/as/applications/{application_id}/webhooks/{webhook_id}/devices/{device_id}/down", + ).Subrouter() router.Use( mux.MiddlewareFunc(webmiddleware.Namespace("applicationserver/io/web")), mux.MiddlewareFunc(webmiddleware.Metadata("Authorization")), @@ -197,7 +202,12 @@ const ( domainHeader = "X-Tts-Domain" ) -func (w *webhooks) createDownlinkURL(ctx context.Context, webhookID *ttnpb.ApplicationWebhookIdentifiers, devID *ttnpb.EndDeviceIdentifiers, op string) string { +func (w *webhooks) downlinkURL( + _ context.Context, + webhookID *ttnpb.ApplicationWebhookIdentifiers, + devID *ttnpb.EndDeviceIdentifiers, + op string, +) string { downlinks := w.downlinks baseURL := downlinks.PublicTLSAddress if baseURL == "" { @@ -212,7 +222,7 @@ func (w *webhooks) createDownlinkURL(ctx context.Context, webhookID *ttnpb.Appli ) } -func (w *webhooks) createDomain(ctx context.Context) string { +func (w *webhooks) domain(_ context.Context) string { downlinks := w.downlinks baseURL := downlinks.PublicTLSAddress if baseURL == "" { @@ -245,6 +255,7 @@ func (w *webhooks) handleUp(ctx context.Context, msg *ttnpb.ApplicationUp) error "location_solved", "service_data", "uplink_message", + "uplink_normalized", }, ) if err != nil { @@ -279,12 +290,14 @@ func (w *webhooks) handleUp(ctx context.Context, msg *ttnpb.ApplicationUp) error return nil } -func activeWebhookUplinkMessage( +func webhookMessage( msg *ttnpb.ApplicationUp, hook *ttnpb.ApplicationWebhook, ) *ttnpb.ApplicationWebhook_Message { switch msg.Up.(type) { case *ttnpb.ApplicationUp_UplinkMessage: return hook.UplinkMessage + case *ttnpb.ApplicationUp_UplinkNormalized: + return hook.UplinkNormalized case *ttnpb.ApplicationUp_JoinAccept: return hook.JoinAccept case *ttnpb.ApplicationUp_DownlinkAck: @@ -311,6 +324,8 @@ func webhookUplinkMessageMask(msg *ttnpb.ApplicationUp) string { switch msg.Up.(type) { case *ttnpb.ApplicationUp_UplinkMessage: return "up.uplink_message" + case *ttnpb.ApplicationUp_UplinkNormalized: + return "up.uplink_normalized" case *ttnpb.ApplicationUp_JoinAccept: return "up.join_accept" case *ttnpb.ApplicationUp_DownlinkAck: @@ -333,12 +348,14 @@ func webhookUplinkMessageMask(msg *ttnpb.ApplicationUp) string { return "" } +// newRequest returns an HTTP request. +// This method returns nil, nil if the hook is not configured for the message. func (w *webhooks) newRequest( ctx context.Context, msg *ttnpb.ApplicationUp, hook *ttnpb.ApplicationWebhook, ) (*http.Request, error) { - cfg := activeWebhookUplinkMessage(msg, hook) + cfg := webhookMessage(msg, hook) if cfg == nil { - return nil, nil + return nil, nil //nolint:nilnil } baseURL, err := expandVariables(hook.BaseUrl, msg) if err != nil { @@ -393,10 +410,10 @@ func (w *webhooks) newRequest( } if hook.DownlinkApiKey != "" { req.Header.Set(downlinkKeyHeader, hook.DownlinkApiKey) - req.Header.Set(downlinkPushHeader, w.createDownlinkURL(ctx, hook.Ids, msg.EndDeviceIds, "push")) - req.Header.Set(downlinkReplaceHeader, w.createDownlinkURL(ctx, hook.Ids, msg.EndDeviceIds, "replace")) + req.Header.Set(downlinkPushHeader, w.downlinkURL(ctx, hook.Ids, msg.EndDeviceIds, "push")) + req.Header.Set(downlinkReplaceHeader, w.downlinkURL(ctx, hook.Ids, msg.EndDeviceIds, "replace")) } - if domain := w.createDomain(ctx); domain != "" { + if domain := w.domain(ctx); domain != "" { req.Header.Set(domainHeader, domain) } req.Header.Set("Content-Type", format.ContentType) @@ -410,7 +427,9 @@ var ( errValidateBody = errors.DefineInvalidArgument("validate_body", "validate body") ) -func (w *webhooks) handleDown(op func(io.Server, context.Context, *ttnpb.EndDeviceIdentifiers, []*ttnpb.ApplicationDownlink) error) http.Handler { +func (w *webhooks) handleDown( + op func(io.Server, context.Context, *ttnpb.EndDeviceIdentifiers, []*ttnpb.ApplicationDownlink) error, +) http.Handler { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { ctx := req.Context() devID := deviceIDFromContext(ctx) diff --git a/pkg/applicationserver/io/web/webhooks_test.go b/pkg/applicationserver/io/web/webhooks_test.go index 88263ff7af..0212715877 100644 --- a/pkg/applicationserver/io/web/webhooks_test.go +++ b/pkg/applicationserver/io/web/webhooks_test.go @@ -51,11 +51,12 @@ func (mockComponent) FromRequestContext(ctx context.Context) context.Context { return ctx } -func createdPooledSink(ctx context.Context, t *testing.T, sink web.Sink) web.Sink { +func pooledSink(ctx context.Context, sink web.Sink) web.Sink { return web.NewPooledSink(ctx, mockComponent{}, sink, 1, 4) } func TestWebhooks(t *testing.T) { + t.Parallel() a, ctx := test.New(t) redisClient, flush := test.NewRedis(ctx, "web_test") @@ -79,6 +80,7 @@ func TestWebhooks(t *testing.T) { // Use a dummy JWT for auth check. longAuthHeader := "Bearer eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoidGVzdHdocm9sZSIsIklzc3VlciI6Iklzc3VlciIsIlVzZXJuYW1lIjoidGVzdHVzZXIiLCJleHAiOjE2NDI0MjU3NDgsImlhdCI6MTY0MjQyNTc0OH0.imuGY_5xnhZYSqjPrc6EUoYV1eapswDBUIBXKVCIYSw" //nolint:lll + //nolint:paralleltest for _, tc := range []struct { prefix string suffix string @@ -114,6 +116,9 @@ func TestWebhooks(t *testing.T) { UplinkMessage: &ttnpb.ApplicationWebhook_Message{ Path: tc.prefix + "up{?devEUI}", }, + UplinkNormalized: &ttnpb.ApplicationWebhook_Message{ + Path: tc.prefix + "up/normalized{?devEUI}", + }, JoinAccept: &ttnpb.ApplicationWebhook_Message{ Path: tc.prefix + "join{?joinEUI}", }, @@ -156,25 +161,27 @@ func TestWebhooks(t *testing.T) { "up.location_solved", "up.service_data", "up.uplink_message", + "up.uplink_normalized", ), }, []string{ "base_url", - "downlink_api_key", "downlink_ack", + "downlink_api_key", "downlink_failed", "downlink_nack", - "downlink_queued", "downlink_queue_invalidated", + "downlink_queued", "downlink_sent", "field_mask", "format", "headers", "ids", - "service_data", "join_accept", "location_solved", + "service_data", "uplink_message", + "uplink_normalized", }, nil }) if err != nil { @@ -182,15 +189,17 @@ func TestWebhooks(t *testing.T) { } t.Run("Upstream", func(t *testing.T) { - baseURL := fmt.Sprintf("https://myapp.com/api/ttn/v3/%s/%s", registeredApplicationID.ApplicationId, registeredDeviceID.DeviceId) + baseURL := fmt.Sprintf( + "https://myapp.com/api/ttn/v3/%s/%s", registeredApplicationID.ApplicationId, registeredDeviceID.DeviceId, + ) testSink := &mockSink{ ch: make(chan *http.Request, 1), } for _, sink := range []web.Sink{ testSink, - createdPooledSink(ctx, t, testSink), - createdPooledSink(ctx, t, - createdPooledSink(ctx, t, testSink), + pooledSink(ctx, testSink), + pooledSink(ctx, + pooledSink(ctx, testSink), ), } { t.Run(fmt.Sprintf("%T", sink), func(t *testing.T) { @@ -224,6 +233,39 @@ func TestWebhooks(t *testing.T) { OK: true, URL: fmt.Sprintf("%s/up?devEUI=%s", baseURL, types.MustEUI64(registeredDeviceID.DevEui)), }, + { + Name: "UplinkNormalized/RegisteredDevice", + Message: &ttnpb.ApplicationUp{ + EndDeviceIds: registeredDeviceID, + Up: &ttnpb.ApplicationUp_UplinkNormalized{ + UplinkNormalized: &ttnpb.ApplicationUplinkNormalized{ + SessionKeyId: []byte{0x11}, + FPort: 42, + FCnt: 42, + FrmPayload: []byte{0x1, 0x2, 0x3}, + NormalizedPayload: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 21.5, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + OK: true, + URL: fmt.Sprintf("%s/up/normalized?devEUI=%s", baseURL, types.MustEUI64(registeredDeviceID.DevEui)), + }, { Name: "UplinkMessage/UnregisteredDevice", Message: &ttnpb.ApplicationUp{ @@ -413,20 +455,21 @@ func TestWebhooks(t *testing.T) { t.Fatalf("Did not expect message but received: %v", req) } case <-time.After(Timeout): - if tc.OK { - t.Fatal("Expected message but nothing received") - } else { + if !tc.OK { return } + t.Fatal("Expected message but nothing received") } a.So(req.URL.String(), should.Equal, tc.URL) a.So(req.Header.Get("Authorization"), should.Equal, longAuthHeader) a.So(req.Header.Get("Content-Type"), should.Equal, "application/json") a.So(req.Header.Get("X-Downlink-Apikey"), should.Equal, "foo.secret") a.So(req.Header.Get("X-Downlink-Push"), should.Equal, - "https://example.com/api/v3/as/applications/foo-app/webhooks/foo-hook/devices/foo-device/down/push") + "https://example.com/api/v3/as/applications/foo-app/webhooks/foo-hook/devices/foo-device/down/push", + ) a.So(req.Header.Get("X-Downlink-Replace"), should.Equal, - "https://example.com/api/v3/as/applications/foo-app/webhooks/foo-hook/devices/foo-device/down/replace") + "https://example.com/api/v3/as/applications/foo-app/webhooks/foo-hook/devices/foo-device/down/replace", //nolint:lll + ) a.So(req.Header.Get("X-Tts-Domain"), should.Equal, "example.com") actualBody, err := stdio.ReadAll(req.Body) if !a.So(err, should.BeNil) { @@ -445,6 +488,7 @@ func TestWebhooks(t *testing.T) { }) } + //nolint:paralleltest t.Run("Downstream", func(t *testing.T) { is, isAddr, closeIS := mockis.New(ctx) defer closeIS() @@ -482,6 +526,7 @@ func TestWebhooks(t *testing.T) { mustHavePeer(ctx, c, ttnpb.ClusterRole_ENTITY_REGISTRY) + //nolint:paralleltest t.Run("Authorization", func(t *testing.T) { for _, tc := range []struct { Name string diff --git a/pkg/applicationserver/observability.go b/pkg/applicationserver/observability.go index d66900db00..4160dcff24 100644 --- a/pkg/applicationserver/observability.go +++ b/pkg/applicationserver/observability.go @@ -56,6 +56,12 @@ var ( events.WithDataType(&ttnpb.ApplicationUplink{}), events.WithPropagateToParent(), ) + evtNormalizeWarningDataUp = events.Define( + "as.up.data.normalize.warning", "normalize uplink data message warning", + events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_TRAFFIC_READ), + events.WithDataType(&ttnpb.ApplicationUplink{}), + events.WithPropagateToParent(), + ) evtReceiveJoinAccept = events.Define( "as.up.join.receive", "receive join-accept message", events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_TRAFFIC_READ), @@ -72,6 +78,12 @@ var ( events.WithDataType(&ttnpb.ApplicationUp{}), events.WithPropagateToParent(), ) + evtForwardNormalizedUp = events.Define( + "as.up.normalized.forward", "forward normalized uplink message", + events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_TRAFFIC_READ), + events.WithDataType(&ttnpb.ApplicationUp{}), + events.WithPropagateToParent(), + ) evtForwardLocationSolved = events.Define( "as.up.location.forward", "forward location solved message", events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_TRAFFIC_READ), @@ -133,7 +145,6 @@ const ( subsystem = "as" unknown = "unknown" networkServer = "network_server" - protocol = "protocol" applicationID = "application_id" ) @@ -245,8 +256,8 @@ func (m messageMetrics) Collect(ch chan<- prometheus.Metric) { func registerReceiveUp(ctx context.Context, msg *ttnpb.ApplicationUp) { ns := "application" - if peer, ok := peer.FromContext(ctx); ok { - ns = peer.Addr.String() + if p, ok := peer.FromContext(ctx); ok { + ns = p.Addr.String() } switch msg.Up.(type) { case *ttnpb.ApplicationUp_JoinAccept: @@ -265,6 +276,8 @@ func registerForwardUp(ctx context.Context, msg *ttnpb.ApplicationUp) { events.Publish(evtForwardJoinAccept.NewWithIdentifiersAndData(ctx, msg.EndDeviceIds, msg)) case *ttnpb.ApplicationUp_UplinkMessage: events.Publish(evtForwardDataUp.NewWithIdentifiersAndData(ctx, msg.EndDeviceIds, msg)) + case *ttnpb.ApplicationUp_UplinkNormalized: + events.Publish(evtForwardNormalizedUp.NewWithIdentifiersAndData(ctx, msg.EndDeviceIds, msg)) case *ttnpb.ApplicationUp_LocationSolved: events.Publish(evtForwardLocationSolved.NewWithIdentifiersAndData(ctx, msg.EndDeviceIds, msg)) case *ttnpb.ApplicationUp_ServiceData: @@ -326,9 +339,12 @@ func registerDropDownlink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, } func (as *ApplicationServer) registerDropDownlinks(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, items []*ttnpb.ApplicationDownlink, err error) { - var errorDetails ttnpb.ErrorDetails - if ttnErr, ok := err.(errors.ErrorDetails); ok { - errorDetails = *ttnpb.ErrorDetailsToProto(ttnErr) + var ( + errorDetails errors.ErrorDetails + pbErrorDetails *ttnpb.ErrorDetails + ) + if errors.As(err, &errorDetails) { + pbErrorDetails = ttnpb.ErrorDetailsToProto(errorDetails) } for _, item := range items { if err := as.publishUp(ctx, &ttnpb.ApplicationUp{ @@ -337,7 +353,7 @@ func (as *ApplicationServer) registerDropDownlinks(ctx context.Context, ids *ttn Up: &ttnpb.ApplicationUp_DownlinkFailed{ DownlinkFailed: &ttnpb.ApplicationDownlinkFailed{ Downlink: item, - Error: &errorDetails, + Error: pbErrorDetails, }, }, }); err != nil { diff --git a/pkg/applicationserver/payload.go b/pkg/applicationserver/payload.go index 484a8150ab..dbb9175f16 100644 --- a/pkg/applicationserver/payload.go +++ b/pkg/applicationserver/payload.go @@ -151,8 +151,13 @@ func (as *ApplicationServer) decodeUplink(ctx context.Context, dev *ttnpb.EndDev if err := as.formatters.DecodeUplink(ctx, dev.Ids, dev.VersionIds, uplink, formatter, parameter); err != nil { log.FromContext(ctx).WithError(err).Warn("Failed to decode uplink") events.Publish(evtDecodeFailDataUp.NewWithIdentifiersAndData(ctx, dev.Ids, err)) - } else if len(uplink.DecodedPayloadWarnings) > 0 { - events.Publish(evtDecodeWarningDataUp.NewWithIdentifiersAndData(ctx, dev.Ids, uplink)) + } else { + if len(uplink.DecodedPayloadWarnings) > 0 { + events.Publish(evtDecodeWarningDataUp.NewWithIdentifiersAndData(ctx, dev.Ids, uplink)) + } + if len(uplink.NormalizedPayloadWarnings) > 0 { + events.Publish(evtNormalizeWarningDataUp.NewWithIdentifiersAndData(ctx, dev.Ids, uplink)) + } } } return nil @@ -211,7 +216,8 @@ func (as *ApplicationServer) decodeDownlink(ctx context.Context, dev *ttnpb.EndD return nil } -func (as *ApplicationServer) locationFromDecodedPayload(uplink *ttnpb.ApplicationUplink) (res *ttnpb.Location) { +func (*ApplicationServer) locationFromPayload(uplink *ttnpb.ApplicationUplink) (res *ttnpb.Location) { + // TODO: Prefer location from normalized payload (https://github.com/TheThingsNetwork/lorawan-stack/issues/5429) m, err := gogoproto.Map(uplink.DecodedPayload) if err != nil { return nil diff --git a/pkg/applicationserver/util_test.go b/pkg/applicationserver/util_test.go index efb1a95542..0ae363eba9 100644 --- a/pkg/applicationserver/util_test.go +++ b/pkg/applicationserver/util_test.go @@ -50,10 +50,6 @@ func mustHavePeer(ctx context.Context, c *component.Component, role ttnpb.Cluste panic("could not connect to peer") } -func eui64Ptr(eui types.EUI64) *types.EUI64 { - return &eui -} - func withDevAddr(ids *ttnpb.EndDeviceIdentifiers, devAddr types.DevAddr) *ttnpb.EndDeviceIdentifiers { newIds := &ttnpb.EndDeviceIdentifiers{} if err := newIds.SetFields(ids, ttnpb.EndDeviceIdentifiersFieldPathsNested...); err != nil { @@ -90,7 +86,11 @@ func startMockNS(ctx context.Context, link chan *mockNSASConn) (*mockNS, string) if err != nil { panic(err) } - go srv.Serve(lis) + go func() { + if err := srv.Serve(lis); err != nil { + panic(err) + } + }() return ns, lis.Addr().String() } @@ -164,7 +164,11 @@ func startMockJS(ctx context.Context) (*mockJS, string) { if err != nil { panic(err) } - go srv.Serve(lis) + go func() { + if err := srv.Serve(lis); err != nil { + panic(err) + } + }() return js, lis.Addr().String() } diff --git a/pkg/messageprocessors/devicerepository/devicerepository.go b/pkg/messageprocessors/devicerepository/devicerepository.go index 156f0aa90a..a916448ccb 100644 --- a/pkg/messageprocessors/devicerepository/devicerepository.go +++ b/pkg/messageprocessors/devicerepository/devicerepository.go @@ -32,9 +32,9 @@ import ( type codecType string const ( - downlinkEncoder = codecType("downlinkEncoder") - downlinkDecoder = codecType("downlinkDecoder") - uplinkDecoder = codecType("uplinkDecoder") + downlinkEncoder codecType = "downlinkEncoder" + downlinkDecoder codecType = "downlinkDecoder" + uplinkDecoder codecType = "uplinkDecoder" // cacheTTL is the TTL for cached payload formatters. cacheTTL = time.Hour @@ -183,7 +183,7 @@ func (h *host) compileProcessor(ctx context.Context, codec codecType, formatter uplinkProcessor: run, }, nil default: - panic(fmt.Sprintf("Invalid codec type: %v", codec)) + panic(fmt.Sprintf("invalid codec type: %v", codec)) } } @@ -207,7 +207,7 @@ func (h *host) compileProcessor(ctx context.Context, codec codecType, formatter }, }, nil default: - panic(fmt.Sprintf("Invalid codec type: %v", codec)) + panic(fmt.Sprintf("invalid codec type: %v", codec)) } } diff --git a/pkg/messageprocessors/javascript/javascript.go b/pkg/messageprocessors/javascript/javascript.go index 1fd32bc690..65f77d2ad7 100644 --- a/pkg/messageprocessors/javascript/javascript.go +++ b/pkg/messageprocessors/javascript/javascript.go @@ -18,12 +18,15 @@ package javascript import ( "context" "fmt" + "reflect" "runtime/trace" "strings" + pbtypes "github.com/gogo/protobuf/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/gogoproto" "go.thethings.network/lorawan-stack/v3/pkg/messageprocessors" + "go.thethings.network/lorawan-stack/v3/pkg/messageprocessors/normalizedpayload" "go.thethings.network/lorawan-stack/v3/pkg/scripting" js "go.thethings.network/lorawan-stack/v3/pkg/scripting/javascript" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" @@ -59,16 +62,17 @@ var ( ) func wrapDownlinkEncoderScript(script string) string { - // Fallback to legacy Encoder() function for backwards compatibility with The Things Network Stack V2 payload functions. + // Fallback to Encoder() for backwards compatibility with The Things Network Stack V2 payload functions. return fmt.Sprintf(` %s function main(input) { + const { data, fPort } = input; if (typeof encodeDownlink === 'function') { - return encodeDownlink(input); + return encodeDownlink({ data, fPort }); } return { - bytes: Encoder(input.data, input.fPort), + bytes: Encoder(data, fPort), fPort: input.fPort } } @@ -76,7 +80,17 @@ func wrapDownlinkEncoderScript(script string) string { } // CompileDownlinkEncoder generates a downlink encoder from the provided script. -func (h *host) CompileDownlinkEncoder(ctx context.Context, script string) (func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDeviceVersionIdentifiers, *ttnpb.ApplicationDownlink) error, error) { +func (h *host) CompileDownlinkEncoder( + ctx context.Context, script string, +) ( + func( + context.Context, + *ttnpb.EndDeviceIdentifiers, + *ttnpb.EndDeviceVersionIdentifiers, + *ttnpb.ApplicationDownlink, + ) error, + error, +) { defer trace.StartRegion(ctx, "compile downlink encoder").End() run, err := h.engine.Compile(ctx, wrapDownlinkEncoderScript(script)) @@ -84,20 +98,35 @@ func (h *host) CompileDownlinkEncoder(ctx context.Context, script string) (func( return nil, err } - return func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationDownlink) error { - return h.encodeDownlink(ctx, ids, version, msg, run) + return func( + ctx context.Context, + ids *ttnpb.EndDeviceIdentifiers, + version *ttnpb.EndDeviceVersionIdentifiers, + msg *ttnpb.ApplicationDownlink, + ) error { + return h.encodeDownlink(ctx, msg, run) }, nil } // EncodeDownlink encodes the message's DecodedPayload to FRMPayload using the given script. -func (h *host) EncodeDownlink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationDownlink, script string) error { +func (h *host) EncodeDownlink( + ctx context.Context, + _ *ttnpb.EndDeviceIdentifiers, + _ *ttnpb.EndDeviceVersionIdentifiers, + msg *ttnpb.ApplicationDownlink, + script string, +) error { run := func(ctx context.Context, fn string, params ...interface{}) (func(interface{}) error, error) { return h.engine.Run(ctx, wrapDownlinkEncoderScript(script), fn, params...) } - return h.encodeDownlink(ctx, ids, version, msg, run) + return h.encodeDownlink(ctx, msg, run) } -func (h *host) encodeDownlink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationDownlink, run func(context.Context, string, ...interface{}) (func(interface{}) error, error)) error { +func (*host) encodeDownlink( + ctx context.Context, + msg *ttnpb.ApplicationDownlink, + run func(context.Context, string, ...interface{}) (func(interface{}) error, error), +) error { defer trace.StartRegion(ctx, "encode downlink message").End() decoded := msg.DecodedPayload @@ -150,28 +179,57 @@ type decodeUplinkOutput struct { Errors []string `json:"errors"` } +type normalizeUplinkOutput struct { + Data interface{} `json:"data"` + Warnings []string `json:"warnings"` + Errors []string `json:"errors"` +} + +type uplinkDecoderOutput struct { + Decoded decodeUplinkOutput `json:"decoded"` + Normalized *normalizeUplinkOutput `json:"normalized"` +} + func wrapUplinkDecoderScript(script string) string { - // Fallback to legacy Decoder() function for backwards compatibility with The Things Network Stack V2 payload functions. + // This wrapper executes decodeUplink() if it is defined. Then, it executes normalizeUplink() if it is defined too, + // and if the output of decodeUplink() didn't return errors. + // Fallback to Decoder() for backwards compatibility with The Things Network Stack V2 payload functions. return fmt.Sprintf(` %s function main(input) { - input = { - bytes: input.bytes.slice(), - fPort: input.fPort, - } + const bytes = input.bytes.slice(); + const { fPort } = input; if (typeof decodeUplink === 'function') { - return decodeUplink(input); + const decoded = decodeUplink({ bytes, fPort }); + let normalized; + const { data, errors } = decoded; + if ((!errors || !errors.length) && data && typeof normalizeUplink === 'function') { + normalized = normalizeUplink({ data }); + } + return { decoded, normalized }; } return { - data: Decoder(input.bytes, input.fPort) + decoded: { + data: Decoder(bytes, fPort) + } } } `, script) } // CompileUplinkDecoder generates an uplink decoder from the provided script. -func (h *host) CompileUplinkDecoder(ctx context.Context, script string) (func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDeviceVersionIdentifiers, *ttnpb.ApplicationUplink) error, error) { +func (h *host) CompileUplinkDecoder( + ctx context.Context, script string, +) ( + func( + context.Context, + *ttnpb.EndDeviceIdentifiers, + *ttnpb.EndDeviceVersionIdentifiers, + *ttnpb.ApplicationUplink, + ) error, + error, +) { defer trace.StartRegion(ctx, "compile uplink decoder").End() run, err := h.engine.Compile(ctx, wrapUplinkDecoderScript(script)) @@ -179,20 +237,35 @@ func (h *host) CompileUplinkDecoder(ctx context.Context, script string) (func(co return nil, err } - return func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationUplink) error { - return h.decodeUplink(ctx, ids, version, msg, run) + return func( + ctx context.Context, + ids *ttnpb.EndDeviceIdentifiers, + version *ttnpb.EndDeviceVersionIdentifiers, + msg *ttnpb.ApplicationUplink, + ) error { + return h.decodeUplink(ctx, msg, run) }, nil } // DecodeUplink decodes the message's FRMPayload to DecodedPayload using the given script. -func (h *host) DecodeUplink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationUplink, script string) error { +func (h *host) DecodeUplink( + ctx context.Context, + _ *ttnpb.EndDeviceIdentifiers, + _ *ttnpb.EndDeviceVersionIdentifiers, + msg *ttnpb.ApplicationUplink, + script string, +) error { run := func(ctx context.Context, fn string, params ...interface{}) (func(interface{}) error, error) { return h.engine.Run(ctx, wrapUplinkDecoderScript(script), fn, params...) } - return h.decodeUplink(ctx, ids, version, msg, run) + return h.decodeUplink(ctx, msg, run) } -func (h *host) decodeUplink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationUplink, run func(context.Context, string, ...interface{}) (func(interface{}) error, error)) error { +func (*host) decodeUplink( + ctx context.Context, + msg *ttnpb.ApplicationUplink, + run func(context.Context, string, ...interface{}) (func(interface{}) error, error), +) error { defer trace.StartRegion(ctx, "decode uplink message").End() input := decodeUplinkInput{ @@ -205,21 +278,72 @@ func (h *host) decodeUplink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers return err } - var output decodeUplinkOutput + var output uplinkDecoderOutput err = valueAs(&output) if err != nil { return errOutput.WithCause(err) } - if len(output.Errors) > 0 { - return errOutputErrors.WithAttributes("errors", strings.Join(output.Errors, ", ")) - } - s, err := gogoproto.Struct(output.Data) + if errs := output.Decoded.Errors; len(errs) > 0 { + return errOutputErrors.WithAttributes("errors", strings.Join(errs, ", ")) + } + decodedPayload, err := gogoproto.Struct(output.Decoded.Data) if err != nil { return errOutput.WithCause(err) } - msg.DecodedPayload = s - msg.DecodedPayloadWarnings = output.Warnings + msg.DecodedPayload, msg.DecodedPayloadWarnings = decodedPayload, output.Decoded.Warnings + + if normalized := output.Normalized; normalized != nil { + if errs := normalized.Errors; len(errs) > 0 { + return errOutputErrors.WithAttributes("errors", strings.Join(errs, ", ")) + } + if normalized.Data == nil { + return errOutput.New() + } + // The returned data can be an array of measurements or a single measurement object. + var measurements []map[string]interface{} + if val := reflect.ValueOf(normalized.Data); val.Kind() == reflect.Slice { + measurements = make([]map[string]interface{}, val.Len()) + for i := 0; i < val.Len(); i++ { + measurement, ok := val.Index(i).Interface().(map[string]interface{}) + if !ok { + return errOutput.New() + } + measurements[i] = measurement + } + } else { + measurement, ok := normalized.Data.(map[string]interface{}) + if !ok { + return errOutput.New() + } + measurements = []map[string]interface{}{measurement} + } + normalizedPayload := make([]*pbtypes.Struct, len(measurements)) + for i := range measurements { + pb, err := gogoproto.Struct(measurements[i]) + if err != nil { + return errOutput.WithCause(err) + } + normalizedPayload[i] = pb + } + // Validate the normalized payload. + _, err := normalizedpayload.Parse(normalizedPayload) + if err != nil { + return errOutput.WithCause(err) + } + msg.NormalizedPayload, msg.NormalizedPayloadWarnings = normalizedPayload, normalized.Warnings + } else { + // If the normalizer is not set, the decoder may return already normalized payload. + // This is a best effort attempt to parse the decoded payload as normalized payload. + // If that does not return an error, the decoded payload is assumed to be normalized. + normalizedPayload := []*pbtypes.Struct{ + decodedPayload, + } + _, err := normalizedpayload.Parse(normalizedPayload) + if err == nil { + msg.NormalizedPayload, msg.NormalizedPayloadWarnings = normalizedPayload, nil + } + } return nil } @@ -239,17 +363,25 @@ func wrapDownlinkDecoderScript(script string) string { %s function main(input) { - input = { - bytes: input.bytes.slice(), - fPort: input.fPort, - } - return decodeDownlink(input); + const bytes = input.bytes.slice(); + const { fPort } = input; + return decodeDownlink({ bytes, fPort }); } `, script) } // CompileDownlinkDecoder generates a downlink decoder from the provided script. -func (h *host) CompileDownlinkDecoder(ctx context.Context, script string) (func(context.Context, *ttnpb.EndDeviceIdentifiers, *ttnpb.EndDeviceVersionIdentifiers, *ttnpb.ApplicationDownlink) error, error) { +func (h *host) CompileDownlinkDecoder( + ctx context.Context, script string, +) ( + func( + context.Context, + *ttnpb.EndDeviceIdentifiers, + *ttnpb.EndDeviceVersionIdentifiers, + *ttnpb.ApplicationDownlink, + ) error, + error, +) { defer trace.StartRegion(ctx, "compile downlink decoder").End() run, err := h.engine.Compile(ctx, wrapDownlinkDecoderScript(script)) @@ -257,20 +389,35 @@ func (h *host) CompileDownlinkDecoder(ctx context.Context, script string) (func( return nil, err } - return func(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationDownlink) error { - return h.decodeDownlink(ctx, ids, version, msg, run) + return func( + ctx context.Context, + ids *ttnpb.EndDeviceIdentifiers, + version *ttnpb.EndDeviceVersionIdentifiers, + msg *ttnpb.ApplicationDownlink, + ) error { + return h.decodeDownlink(ctx, msg, run) }, nil } // DecodeUplink decodes the message's FRMPayload to DecodedPayload using the given script. -func (h *host) DecodeDownlink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationDownlink, script string) error { +func (h *host) DecodeDownlink( + ctx context.Context, + _ *ttnpb.EndDeviceIdentifiers, + _ *ttnpb.EndDeviceVersionIdentifiers, + msg *ttnpb.ApplicationDownlink, + script string, +) error { run := func(ctx context.Context, fn string, params ...interface{}) (func(interface{}) error, error) { return h.engine.Run(ctx, wrapDownlinkDecoderScript(script), fn, params...) } - return h.decodeDownlink(ctx, ids, version, msg, run) + return h.decodeDownlink(ctx, msg, run) } -func (h *host) decodeDownlink(ctx context.Context, ids *ttnpb.EndDeviceIdentifiers, version *ttnpb.EndDeviceVersionIdentifiers, msg *ttnpb.ApplicationDownlink, run func(context.Context, string, ...interface{}) (func(interface{}) error, error)) error { +func (*host) decodeDownlink( + ctx context.Context, + msg *ttnpb.ApplicationDownlink, + run func(context.Context, string, ...interface{}) (func(interface{}) error, error), +) error { defer trace.StartRegion(ctx, "decode downlink message").End() input := decodeDownlinkInput{ diff --git a/pkg/messageprocessors/javascript/javascript_test.go b/pkg/messageprocessors/javascript/javascript_test.go index b9a3fbe8ee..86a1d43c81 100644 --- a/pkg/messageprocessors/javascript/javascript_test.go +++ b/pkg/messageprocessors/javascript/javascript_test.go @@ -19,7 +19,9 @@ import ( pbtypes "github.com/gogo/protobuf/types" "github.com/smartystreets/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/gogoproto" + "go.thethings.network/lorawan-stack/v3/pkg/messageprocessors/normalizedpayload" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" @@ -27,6 +29,7 @@ import ( ) func TestLegacyEncodeDownlink(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := test.Context() @@ -96,6 +99,7 @@ func TestLegacyEncodeDownlink(t *testing.T) { } func TestEncodeDownlink(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := test.Context() @@ -231,6 +235,7 @@ func TestEncodeDownlink(t *testing.T) { } func TestLegacyDecodeUplink(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := test.Context() @@ -308,7 +313,12 @@ func TestLegacyDecodeUplink(t *testing.T) { } } +func float64Ptr(f float64) *float64 { + return &f +} + func TestDecodeUplink(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := test.Context() @@ -324,10 +334,10 @@ func TestDecodeUplink(t *testing.T) { } message := &ttnpb.ApplicationUplink{ - FrmPayload: []byte{0xF7, 0xAE}, + FrmPayload: []byte{0xF7, 0xAE, 0xF7, 0xD8}, } - // Decode bytes. + // Decode and normalize a single measurement with a decoder warning. { script := ` function decodeUplink(input) { @@ -343,15 +353,194 @@ func TestDecodeUplink(t *testing.T) { warnings: warnings } } + + function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.temperature + } + } + } + } ` err := host.DecodeUplink(ctx, ids, nil, message, script) a.So(err, should.BeNil) - m, err := gogoproto.Map(message.DecodedPayload) + + decodedPayload, err := gogoproto.Map(message.DecodedPayload) a.So(err, should.BeNil) - a.So(m, should.Resemble, map[string]interface{}{ + a.So(decodedPayload, should.Resemble, map[string]interface{}{ "temperature": -21.3, }) a.So(message.DecodedPayloadWarnings, should.Resemble, []string{"it's cold"}) + + a.So(message.NormalizedPayload, should.HaveLength, 1) + measurements, err := normalizedpayload.Parse(message.NormalizedPayload) + a.So(err, should.BeNil) + a.So(measurements[0], should.Resemble, normalizedpayload.Measurement{ + Air: &normalizedpayload.Air{ + Temperature: float64Ptr(-21.3), + }, + }) + } + + // Decode a single measurement that is already normalized. + { + //nolint:lll + script := ` + function decodeUplink(input) { + return { + data: { + air: { + temperature: (((input.bytes[0] & 0x80 ? input.bytes[0] - 0x100 : input.bytes[0]) << 8) | input.bytes[1]) / 100 + } + } + } + } + ` + err := host.DecodeUplink(ctx, ids, nil, message, script) + a.So(err, should.BeNil) + a.So(message.NormalizedPayload, should.HaveLength, 1) + a.So(message.DecodedPayload, should.Resemble, message.NormalizedPayload[0]) + + measurements, err := normalizedpayload.Parse(message.NormalizedPayload) + a.So(err, should.BeNil) + a.So(measurements[0], should.Resemble, normalizedpayload.Measurement{ + Air: &normalizedpayload.Air{ + Temperature: float64Ptr(-21.3), + }, + }) + } + + // Decode and normalize two measurements. + { + script := ` + function decodeUplink(input) { + var data = { + temperatures: [] + }; + for (var i = 0; i < input.bytes.length; i += 2) { + var temp = (((input.bytes[i] & 0x80 ? input.bytes[i] - 0x100 : input.bytes[i]) << 8) | input.bytes[i+1]) / 100; + data.temperatures.push(temp); + } + return { + data, + } + } + + function normalizeUplink(input) { + return { + data: input.data.temperatures.map((d) => { + return { + air: { + temperature: d + } + } + }) + } + } + ` + err := host.DecodeUplink(ctx, ids, nil, message, script) + a.So(err, should.BeNil) + + decodedPayload, err := gogoproto.Map(message.DecodedPayload) + a.So(err, should.BeNil) + a.So(decodedPayload, should.Resemble, map[string]interface{}{ + "temperatures": []interface{}{-21.3, -20.88}, + }) + + a.So(message.NormalizedPayload, should.HaveLength, 2) + measurements, err := normalizedpayload.Parse(message.NormalizedPayload) + a.So(err, should.BeNil) + a.So(measurements, should.Resemble, []normalizedpayload.Measurement{ + { + Air: &normalizedpayload.Air{ + Temperature: float64Ptr(-21.3), + }, + }, + { + Air: &normalizedpayload.Air{ + Temperature: float64Ptr(-20.88), + }, + }, + }) + } + + // Return errors from decoder and ensure that the normalizer isn't called. + { + script := ` + function decodeUplink(input) { + return { + errors: ["test error"] + } + } + + function normalizeUplink(input) { + throw new Error("this should not be called") + } + ` + err := host.DecodeUplink(ctx, ids, nil, message, script) + a.So(err, should.NotBeNil) + a.So(errors.IsAborted(err), should.BeTrue) + } + + // Decode and normalize a single measurement with a normalizer error. + { + script := ` + function decodeUplink(input) { + var data = { + temperature: (((input.bytes[0] & 0x80 ? input.bytes[0] - 0x100 : input.bytes[0]) << 8) | input.bytes[1]) / 100 + } + var warnings = []; + if (data.temperature < -10) { + warnings.push("it's cold"); + } + return { + data: data, + warnings: warnings + } + } + + function normalizeUplink(input) { + return { + errors: ["test error"] + } + } + ` + err := host.DecodeUplink(ctx, ids, nil, message, script) + a.So(err, should.NotBeNil) + a.So(errors.IsAborted(err), should.BeTrue) + } + + // Decode and normalize a single measurement with out-of-range value. + { + message := &ttnpb.ApplicationUplink{ + FPort: 4, + FrmPayload: []byte{0x80, 0x42}, // Temperature is -327.02 °C which is below absolute zero + } + //nolint:lll + script := ` + function decodeUplink(input) { + return { + data: { + temperature: (((input.bytes[0] & 0x80 ? input.bytes[0] - 0x100 : input.bytes[0]) << 8) | input.bytes[1]) / 100 + } + } + } + + function normalizeUplink(input) { + return { + data: { + air: { + temperature: input.data.temperature + } + } + } + } + ` + err := host.DecodeUplink(ctx, ids, nil, message, script) + a.So(err, should.NotBeNil) + a.So(errors.IsInvalidArgument(err), should.BeTrue) } // The Things Node example. @@ -360,6 +549,7 @@ func TestDecodeUplink(t *testing.T) { FPort: 4, FrmPayload: []byte{0x0C, 0xB2, 0x04, 0x80, 0xF7, 0xAE}, } + //nolint:lll script := ` function decodeUplink(input) { var data = {}; @@ -444,6 +634,7 @@ func TestDecodeUplink(t *testing.T) { } func TestDecodeDownlink(t *testing.T) { + t.Parallel() a := assertions.New(t) ctx := test.Context() diff --git a/pkg/messageprocessors/normalizedpayload/uplink.go b/pkg/messageprocessors/normalizedpayload/uplink.go new file mode 100644 index 0000000000..a8fc4fe6fe --- /dev/null +++ b/pkg/messageprocessors/normalizedpayload/uplink.go @@ -0,0 +1,263 @@ +// Copyright © 2022 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 normalizedpayload implements functionality for parsing and validating normalized payload. +package normalizedpayload + +import ( + "fmt" + "time" + + pbtypes "github.com/gogo/protobuf/types" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "golang.org/x/exp/constraints" +) + +// Air is an air measurement. +type Air struct { + Temperature *float64 + RelativeHumidity *float64 + Pressure *float64 +} + +// Wind is a wind measurement. +type Wind struct { + Speed *float64 + Direction *float64 +} + +// Measurement is a measurement. +type Measurement struct { + Time *time.Time + Air *Air + Wind *Wind +} + +var ( + errFieldType = errors.DefineInvalidArgument("field_type", "invalid field type of `{path}`") + errFieldMinimum = errors.DefineInvalidArgument( + "field_minimum", + "`{path}` should be equal or greater than `{minimum}`", + ) + //nolint:unused + errFieldExclusiveMinimum = errors.DefineInvalidArgument( + "field_exclusive_minimum", + "`{path}` should be greater than `{minimum}`", + ) + errFieldMaximum = errors.DefineInvalidArgument( + "field_maximum", + "`{path}` should be equal or less than `{maximum}`", + ) + errFieldExclusiveMaximum = errors.DefineInvalidArgument( + "field_exclusive_maximum", + "`{path}` should be less than `{maximum}`", + ) + errUnknownField = errors.DefineInvalidArgument("unknown_field", "unknown field `{path}`") +) + +type fieldParser func(dst *Measurement, src *pbtypes.Value, path string) error + +// object validates that the path is a structure and sets the target to an empty value. +func object[T any](selector func(*Measurement) **T) fieldParser { + return func(dst *Measurement, src *pbtypes.Value, path string) error { + _, ok := src.Kind.(*pbtypes.Value_StructValue) + if !ok { + return errFieldType.WithAttributes("path", path) + } + *selector(dst) = new(T) + return nil + } +} + +type fieldValidator[T any] func(v T, path string) error + +func validate[T any](val T, validators []fieldValidator[T], path string) error { + for _, v := range validators { + if err := v(val, path); err != nil { + return err + } + } + return nil +} + +// parseTime parses and validates the time. The input value must be RFC3339. +func parseTime(selector func(dst *Measurement) **time.Time, vals ...fieldValidator[time.Time]) fieldParser { + return func(dst *Measurement, src *pbtypes.Value, path string) error { + val, ok := src.Kind.(*pbtypes.Value_StringValue) + if !ok { + return errFieldType.WithAttributes("path", path) + } + t, err := time.Parse(time.RFC3339Nano, val.StringValue) + if err != nil { + return err + } + if err := validate(t, vals, path); err != nil { + return err + } + *selector(dst) = &t + return nil + } +} + +// parseNumber parses and validates a number. +func parseNumber(selector func(dst *Measurement) **float64, vals ...fieldValidator[float64]) fieldParser { + return func(dst *Measurement, src *pbtypes.Value, path string) error { + val, ok := src.Kind.(*pbtypes.Value_NumberValue) + if !ok { + return errFieldType.WithAttributes("path", path) + } + n := val.NumberValue + if err := validate(n, vals, path); err != nil { + return err + } + *selector(dst) = &n + return nil + } +} + +// minimum returns a field validator that checks the inclusive minimum. +func minimum[T constraints.Ordered](min T) fieldValidator[T] { + return func(v T, path string) error { + if v < min { + return errFieldMinimum.WithAttributes( + "path", path, + "minimum", min, + ) + } + return nil + } +} + +// exclusiveMinimum returns a field validator that checks the exclusive minimum. +// +//nolint:unused,deadcode +func exclusiveMinimum[T constraints.Ordered](min T) fieldValidator[T] { + return func(v T, path string) error { + if v <= min { + return errFieldExclusiveMinimum.WithAttributes( + "path", path, + "minimum", min, + ) + } + return nil + } +} + +// maximum returns a field validator that checks the inclusive maximum. +func maximum[T constraints.Ordered](max T) fieldValidator[T] { + return func(v T, path string) error { + if v > max { + return errFieldMaximum.WithAttributes( + "path", path, + "maximum", max, + ) + } + return nil + } +} + +// exclusiveMaximum returns a field validator that checks the exclusive maximum. +func exclusiveMaximum[T constraints.Ordered](max T) fieldValidator[T] { + return func(v T, path string) error { + if v >= max { + return errFieldExclusiveMaximum.WithAttributes( + "path", path, + "maximum", max, + ) + } + return nil + } +} + +var fieldParsers = map[string]fieldParser{ + "time": parseTime( + func(dst *Measurement) **time.Time { + return &dst.Time + }, + ), + "air": object( + func(dst *Measurement) **Air { + return &dst.Air + }, + ), + "air.temperature": parseNumber( + func(dst *Measurement) **float64 { + return &dst.Air.Temperature + }, + minimum(-273.15), + ), + "air.relativeHumidity": parseNumber( + func(dst *Measurement) **float64 { + return &dst.Air.RelativeHumidity + }, + minimum(0.0), + maximum(100.0), + ), + "air.pressure": parseNumber( + func(dst *Measurement) **float64 { + return &dst.Air.Pressure + }, + minimum(900.0), + maximum(1100.0), + ), + "wind": object( + func(dst *Measurement) **Wind { + return &dst.Wind + }, + ), + "wind.speed": parseNumber( + func(dst *Measurement) **float64 { + return &dst.Wind.Speed + }, + minimum(0.0), + ), + "wind.direction": parseNumber( + func(dst *Measurement) **float64 { + return &dst.Wind.Direction + }, + minimum(0.0), + exclusiveMaximum(360.0), + ), +} + +// Parse parses and validates the measurements. +func Parse(measurements []*pbtypes.Struct) ([]Measurement, error) { + res := make([]Measurement, len(measurements)) + for i, src := range measurements { + err := parse(&res[i], src, "") + if err != nil { + return nil, err + } + } + return res, nil +} + +func parse(dst *Measurement, src *pbtypes.Struct, prefix string) error { + for k, v := range src.GetFields() { + path := fmt.Sprintf("%s%s", prefix, k) + parser, ok := fieldParsers[path] + if !ok { + return errUnknownField.WithAttributes("path", path) + } + if err := parser(dst, v, path); err != nil { + return err + } + if s, ok := v.Kind.(*pbtypes.Value_StructValue); ok { + if err := parse(dst, s.StructValue, path+"."); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/messageprocessors/normalizedpayload/uplink_test.go b/pkg/messageprocessors/normalizedpayload/uplink_test.go new file mode 100644 index 0000000000..5f847f46ad --- /dev/null +++ b/pkg/messageprocessors/normalizedpayload/uplink_test.go @@ -0,0 +1,223 @@ +// Copyright © 2022 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 normalizedpayload_test + +import ( + "testing" + "time" + + pbtypes "github.com/gogo/protobuf/types" + "github.com/smartystreets/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/messageprocessors/normalizedpayload" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func timePtr(t time.Time) *time.Time { + return &t +} + +func float64Ptr(f float64) *float64 { + return &f +} + +func TestUplink(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + normalizedPayload []*pbtypes.Struct + expected []normalizedpayload.Measurement + errorAssertion func(error) bool + }{ + { + name: "single timestamp", + normalizedPayload: []*pbtypes.Struct{ + { + Fields: map[string]*pbtypes.Value{ + "time": { + Kind: &pbtypes.Value_StringValue{ + StringValue: "2022-08-23T17:13:42Z", + }, + }, + }, + }, + }, + expected: []normalizedpayload.Measurement{ + { + Time: timePtr(time.Date(2022, 8, 23, 17, 13, 42, 0, time.UTC)), + }, + }, + }, + { + name: "two air temperatures", + normalizedPayload: []*pbtypes.Struct{ + { + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 20.42, + }, + }, + }, + }, + }, + }, + }, + }, + { + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 19.61, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []normalizedpayload.Measurement{ + { + Air: &normalizedpayload.Air{ + Temperature: float64Ptr(20.42), + }, + }, + { + Air: &normalizedpayload.Air{ + Temperature: float64Ptr(19.61), + }, + }, + }, + }, + { + name: "below absolute zero", + normalizedPayload: []*pbtypes.Struct{ + { + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: -300, + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorAssertion: errors.IsInvalidArgument, + }, + { + name: "invalid direction", + normalizedPayload: []*pbtypes.Struct{ + { + Fields: map[string]*pbtypes.Value{ + "wind": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "direction": { + Kind: &pbtypes.Value_NumberValue{ + NumberValue: 360, // this is 0 degrees + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorAssertion: errors.IsInvalidArgument, + }, + { + name: "invalid type", + normalizedPayload: []*pbtypes.Struct{ + { + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "temperature": { + Kind: &pbtypes.Value_StringValue{ + StringValue: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorAssertion: errors.IsInvalidArgument, + }, + { + name: "unknown field", + normalizedPayload: []*pbtypes.Struct{ + { + Fields: map[string]*pbtypes.Value{ + "air": { + Kind: &pbtypes.Value_StructValue{ + StructValue: &pbtypes.Struct{ + Fields: map[string]*pbtypes.Value{ + "unknown": { + Kind: &pbtypes.Value_StringValue{ + StringValue: "test", + }, + }, + }, + }, + }, + }, + }, + }, + }, + errorAssertion: errors.IsInvalidArgument, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + measurements, err := normalizedpayload.Parse(tc.normalizedPayload) + if tc.errorAssertion != nil { + a.So(err, should.NotBeNil) + a.So(tc.errorAssertion(err), should.BeTrue) + } else { + a.So(err, should.BeNil) + a.So(measurements, should.Resemble, tc.expected) + } + }) + } +} diff --git a/pkg/ttnpb/applicationserver.pb.paths.fm.go b/pkg/ttnpb/applicationserver.pb.paths.fm.go index 4a54879d9a..a99451c177 100644 --- a/pkg/ttnpb/applicationserver.pb.paths.fm.go +++ b/pkg/ttnpb/applicationserver.pb.paths.fm.go @@ -187,6 +187,8 @@ var DecodeUplinkRequestFieldPathsNested = []string{ "uplink.network_ids.net_id", "uplink.network_ids.tenant_address", "uplink.network_ids.tenant_id", + "uplink.normalized_payload", + "uplink.normalized_payload_warnings", "uplink.received_at", "uplink.rx_metadata", "uplink.session_key_id", @@ -260,6 +262,8 @@ var DecodeUplinkResponseFieldPathsNested = []string{ "uplink.network_ids.net_id", "uplink.network_ids.tenant_address", "uplink.network_ids.tenant_id", + "uplink.normalized_payload", + "uplink.normalized_payload_warnings", "uplink.received_at", "uplink.rx_metadata", "uplink.session_key_id", diff --git a/pkg/ttnpb/applicationserver_pubsub.pb.go b/pkg/ttnpb/applicationserver_pubsub.pb.go index e091fe77c5..e6418da20f 100644 --- a/pkg/ttnpb/applicationserver_pubsub.pb.go +++ b/pkg/ttnpb/applicationserver_pubsub.pb.go @@ -127,6 +127,7 @@ type ApplicationPubSub struct { // The topic to which the Application Server subscribes for downlink queue replace operations. DownlinkReplace *ApplicationPubSub_Message `protobuf:"bytes,8,opt,name=downlink_replace,json=downlinkReplace,proto3" json:"downlink_replace,omitempty"` UplinkMessage *ApplicationPubSub_Message `protobuf:"bytes,9,opt,name=uplink_message,json=uplinkMessage,proto3" json:"uplink_message,omitempty"` + UplinkNormalized *ApplicationPubSub_Message `protobuf:"bytes,20,opt,name=uplink_normalized,json=uplinkNormalized,proto3" json:"uplink_normalized,omitempty"` JoinAccept *ApplicationPubSub_Message `protobuf:"bytes,10,opt,name=join_accept,json=joinAccept,proto3" json:"join_accept,omitempty"` DownlinkAck *ApplicationPubSub_Message `protobuf:"bytes,11,opt,name=downlink_ack,json=downlinkAck,proto3" json:"downlink_ack,omitempty"` DownlinkNack *ApplicationPubSub_Message `protobuf:"bytes,12,opt,name=downlink_nack,json=downlinkNack,proto3" json:"downlink_nack,omitempty"` @@ -267,6 +268,13 @@ func (m *ApplicationPubSub) GetUplinkMessage() *ApplicationPubSub_Message { return nil } +func (m *ApplicationPubSub) GetUplinkNormalized() *ApplicationPubSub_Message { + if m != nil { + return m.UplinkNormalized + } + return nil +} + func (m *ApplicationPubSub) GetJoinAccept() *ApplicationPubSub_Message { if m != nil { return m.JoinAccept @@ -1061,145 +1069,146 @@ func init() { } var fileDescriptor_1dce56ec18597200 = []byte{ - // 2194 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x59, 0x41, 0x6f, 0xdb, 0xc8, - 0x15, 0xce, 0x58, 0xb1, 0x6c, 0x3d, 0xc9, 0xb6, 0x32, 0xbb, 0x6d, 0x68, 0x65, 0x93, 0x75, 0xb5, - 0xc6, 0xae, 0xec, 0x84, 0x52, 0x22, 0x77, 0xb7, 0x1b, 0x05, 0x45, 0x22, 0xda, 0x4e, 0xec, 0x26, - 0x71, 0x62, 0x4a, 0x8b, 0x6d, 0x12, 0x27, 0xc4, 0x48, 0x1c, 0xcb, 0x8c, 0x29, 0x92, 0xe1, 0x0c, - 0xed, 0x78, 0x93, 0x00, 0xc1, 0x5e, 0x5a, 0xf4, 0x50, 0x2c, 0xd0, 0x43, 0x51, 0x14, 0x45, 0x81, - 0x16, 0x68, 0x81, 0x1c, 0x8b, 0x9e, 0xbb, 0x3d, 0x16, 0x05, 0x8a, 0x1e, 0xfa, 0x13, 0x5a, 0x14, - 0x68, 0x2f, 0x05, 0xf6, 0x54, 0xb8, 0x40, 0x51, 0x70, 0x48, 0x8a, 0xb4, 0x95, 0x8d, 0x2d, 0x07, - 0x3d, 0xec, 0x25, 0x79, 0xc3, 0xf7, 0xde, 0x37, 0xdf, 0x7b, 0xf3, 0x34, 0x6f, 0x66, 0x0c, 0xe7, - 0x4d, 0xdb, 0x25, 0xdb, 0xc4, 0x92, 0x19, 0x27, 0xed, 0xcd, 0x0a, 0x71, 0x8c, 0x0a, 0x71, 0x1c, - 0xd3, 0x68, 0x13, 0x6e, 0xd8, 0x16, 0xa3, 0xee, 0x16, 0x75, 0x35, 0xc7, 0x6b, 0x31, 0xaf, 0x55, - 0x76, 0x5c, 0x9b, 0xdb, 0x78, 0x9c, 0x73, 0xab, 0x1c, 0x7a, 0x95, 0xb7, 0xe6, 0x0a, 0x0b, 0x1d, - 0x83, 0x6f, 0x78, 0xad, 0x72, 0xdb, 0xee, 0x56, 0x9a, 0x1b, 0xb4, 0xb9, 0x61, 0x58, 0x1d, 0xb6, - 0x6c, 0xe9, 0x1e, 0xe3, 0xae, 0x41, 0x59, 0x45, 0x78, 0xb5, 0xe5, 0x0e, 0xb5, 0xe4, 0x8e, 0x2d, - 0xaf, 0x9b, 0xa4, 0xc3, 0x2a, 0xc4, 0xb2, 0x6c, 0x1e, 0xcc, 0x10, 0xa0, 0x16, 0xea, 0x09, 0x14, - 0x6a, 0x6d, 0xd9, 0x3b, 0x8e, 0x6b, 0x3f, 0xde, 0x49, 0x3a, 0x6f, 0x11, 0xd3, 0xd0, 0x09, 0xa7, - 0x95, 0x3e, 0x21, 0x84, 0x90, 0x13, 0x10, 0x1d, 0xbb, 0x63, 0x07, 0xce, 0x2d, 0x6f, 0x5d, 0x8c, - 0xc4, 0x40, 0x48, 0xa1, 0xf9, 0xfc, 0x40, 0xbc, 0x1f, 0x32, 0xdb, 0x7a, 0x09, 0xed, 0xb7, 0x3a, - 0xb6, 0xdd, 0x31, 0x69, 0x90, 0xb7, 0x3e, 0xed, 0x99, 0x50, 0xdb, 0x23, 0xa2, 0x7b, 0xae, 0x30, - 0x08, 0xf5, 0xa7, 0xf6, 0xeb, 0x69, 0xd7, 0xe1, 0x3b, 0xa1, 0x72, 0x6a, 0xbf, 0x72, 0xdd, 0xa0, - 0xa6, 0xae, 0x75, 0x09, 0xdb, 0x0c, 0x2d, 0xde, 0xde, 0x6f, 0xc1, 0x8d, 0x2e, 0x65, 0x9c, 0x74, - 0x9d, 0xd0, 0xe0, 0x9d, 0xfe, 0xc5, 0x35, 0x74, 0x6a, 0x71, 0x63, 0xdd, 0xa0, 0x6e, 0x48, 0xb2, - 0xf8, 0x47, 0x04, 0x6f, 0xd5, 0xe3, 0x25, 0xbf, 0xed, 0xb5, 0x1a, 0x5e, 0x6b, 0x39, 0x36, 0xc3, - 0x77, 0x60, 0x22, 0x51, 0x12, 0x9a, 0xa1, 0x33, 0x09, 0x4d, 0xa1, 0x52, 0xb6, 0xfa, 0x6e, 0x79, - 0x6f, 0x29, 0x94, 0x13, 0x30, 0x09, 0x00, 0x65, 0x74, 0x57, 0x19, 0xfe, 0x01, 0x1a, 0xca, 0x23, - 0x75, 0x9c, 0x24, 0x2d, 0x18, 0x5e, 0x04, 0x70, 0xbc, 0x96, 0xc6, 0xbc, 0x96, 0x66, 0xe8, 0xd2, - 0xd0, 0x14, 0x2a, 0x65, 0x94, 0xf7, 0x76, 0x95, 0x69, 0xb7, 0x28, 0x4d, 0x57, 0xcf, 0x3c, 0xb8, - 0x47, 0xe4, 0x4f, 0xce, 0xcb, 0x17, 0xef, 0x97, 0x2e, 0xd7, 0xee, 0xc9, 0xf7, 0x2f, 0x47, 0xc3, - 0x99, 0x27, 0xd5, 0x73, 0xcf, 0xa6, 0xd5, 0x51, 0x27, 0xa4, 0x5a, 0x1b, 0xfd, 0xe2, 0xc5, 0xe4, - 0xf1, 0xd1, 0x63, 0x79, 0x54, 0xfc, 0xc7, 0x14, 0x9c, 0xe8, 0x0b, 0x06, 0xdf, 0x86, 0x54, 0xcc, - 0xfa, 0xdc, 0x2b, 0x58, 0xf7, 0x05, 0xaf, 0xe4, 0x23, 0xee, 0xc1, 0x14, 0x25, 0xa4, 0xfa, 0x50, - 0x78, 0x1e, 0xa0, 0xed, 0x52, 0xc2, 0xa9, 0xae, 0x11, 0x2e, 0x88, 0x67, 0xab, 0x85, 0x72, 0xb0, - 0x1e, 0xe5, 0x68, 0x3d, 0xca, 0xcd, 0x68, 0x3d, 0x94, 0x88, 0xe1, 0x31, 0x35, 0x13, 0xfa, 0xd5, - 0xb9, 0x0f, 0xe2, 0x39, 0x7a, 0x04, 0x92, 0x1a, 0x04, 0x24, 0xf4, 0xab, 0x73, 0x7c, 0x19, 0xd2, - 0xeb, 0xb6, 0xdb, 0x25, 0x5c, 0x3a, 0x9e, 0x4c, 0xdf, 0x9b, 0x07, 0xa6, 0x2f, 0x74, 0xc3, 0x0b, - 0x70, 0xdc, 0x22, 0x9c, 0x49, 0x27, 0xc4, 0xfc, 0xe5, 0x03, 0xb3, 0x53, 0x5e, 0xa9, 0x37, 0x1b, - 0xb7, 0x5d, 0x7b, 0xcb, 0xd0, 0xa9, 0xbb, 0x74, 0x4c, 0x15, 0xde, 0x3e, 0x4a, 0xf7, 0x11, 0xe7, - 0xd2, 0xe4, 0x61, 0x51, 0x6e, 0xae, 0x36, 0x9b, 0x49, 0x14, 0xdf, 0x1b, 0x5f, 0x87, 0x11, 0xb2, - 0xcd, 0x34, 0xc3, 0xe6, 0x12, 0x15, 0x40, 0xe7, 0x0f, 0x06, 0xaa, 0x7f, 0xdc, 0x58, 0xb6, 0x93, - 0x50, 0x69, 0xb2, 0xcd, 0x96, 0x6d, 0x8e, 0xdf, 0x05, 0x68, 0x11, 0x46, 0x35, 0x6e, 0x3b, 0x46, - 0x5b, 0x4a, 0x8b, 0xec, 0x8c, 0xec, 0x2a, 0xc7, 0xdd, 0x21, 0x49, 0x57, 0x33, 0xbe, 0xaa, 0xe9, - 0x6b, 0xf0, 0x0a, 0x8c, 0xe9, 0xf6, 0xb6, 0x65, 0x1a, 0xd6, 0xa6, 0xe6, 0x78, 0x6c, 0x43, 0x1a, - 0x11, 0x53, 0xcf, 0x1c, 0x22, 0x06, 0xca, 0x18, 0xe9, 0x50, 0x35, 0x17, 0xf9, 0xdf, 0xf6, 0xd8, - 0x06, 0x6e, 0x42, 0xbe, 0x87, 0xe7, 0x52, 0xc7, 0x24, 0x6d, 0x2a, 0x8d, 0x0e, 0x0a, 0x39, 0x11, - 0x41, 0xa8, 0x01, 0x02, 0xbe, 0x0d, 0xe3, 0x9e, 0x23, 0x30, 0xbb, 0x81, 0x89, 0x94, 0x19, 0x14, - 0x73, 0x2c, 0x00, 0x08, 0x87, 0xf8, 0x3b, 0x90, 0x7d, 0x68, 0x1b, 0x96, 0x46, 0xda, 0x6d, 0xea, - 0x70, 0x09, 0x06, 0x85, 0x03, 0xdf, 0xbb, 0x2e, 0x9c, 0xf1, 0x0d, 0xe8, 0xe5, 0x40, 0x23, 0xed, - 0x4d, 0x29, 0x3b, 0x28, 0x58, 0x36, 0x72, 0xaf, 0xb7, 0x37, 0xf7, 0xac, 0x88, 0xe5, 0xc3, 0xe5, - 0x8e, 0xbc, 0x22, 0x2b, 0x64, 0x1f, 0x1e, 0xa3, 0x16, 0x97, 0xc6, 0x8e, 0x8c, 0xd7, 0xa0, 0x16, - 0xc7, 0x2a, 0xf4, 0x96, 0x47, 0x5b, 0x27, 0x86, 0x49, 0x75, 0x69, 0x7c, 0x50, 0xc4, 0xf1, 0x08, - 0xe1, 0xaa, 0x00, 0xd8, 0x83, 0xf9, 0xc8, 0xa3, 0x1e, 0xd5, 0xa5, 0x89, 0x23, 0x63, 0xae, 0x0a, - 0x00, 0xdc, 0x81, 0xc2, 0x5e, 0x4c, 0xcd, 0xb0, 0xa2, 0xa6, 0xa9, 0x4b, 0x6f, 0x0c, 0x0a, 0x2f, - 0xed, 0x81, 0x5f, 0x8e, 0xa1, 0x7c, 0xf2, 0xa6, 0x1d, 0xf6, 0x07, 0x66, 0x9b, 0x5b, 0x54, 0x97, - 0xf2, 0x03, 0x93, 0x8f, 0x10, 0x1a, 0x02, 0xc0, 0x2f, 0x29, 0xff, 0xf8, 0x61, 0xb4, 0xa9, 0xa6, - 0x13, 0x4e, 0x24, 0x3c, 0x70, 0x49, 0x85, 0xee, 0x0b, 0x84, 0x93, 0x42, 0x1d, 0x72, 0xc9, 0x7d, - 0x0b, 0xbf, 0x07, 0x10, 0x1e, 0x6e, 0x3c, 0xd7, 0x14, 0x9d, 0x21, 0x23, 0xfa, 0x94, 0x9b, 0xfa, - 0x3e, 0x42, 0x6a, 0x26, 0xd0, 0x7d, 0xe4, 0x9a, 0x61, 0x6f, 0x41, 0x79, 0x54, 0xf8, 0x6d, 0x06, - 0x72, 0xc9, 0x5d, 0xeb, 0xd0, 0x18, 0x78, 0x1a, 0x32, 0x6d, 0xd3, 0xa0, 0x16, 0x8f, 0xbb, 0x5c, - 0xb8, 0x11, 0x9d, 0x54, 0x47, 0x03, 0xcd, 0xb2, 0x8e, 0xdf, 0x81, 0x51, 0x8f, 0x51, 0xd7, 0x22, - 0x5d, 0x2a, 0x9a, 0x41, 0x62, 0xb7, 0xea, 0x29, 0x7c, 0x23, 0x87, 0x30, 0xb6, 0x6d, 0xbb, 0x7a, - 0xb8, 0xe1, 0xc7, 0x46, 0x91, 0x02, 0x7f, 0x0c, 0x63, 0xcc, 0x6b, 0xb1, 0xb6, 0x6b, 0xb4, 0xa8, - 0xf6, 0xc8, 0x66, 0xd2, 0xf0, 0x14, 0x2a, 0x8d, 0x57, 0xab, 0x83, 0xed, 0xca, 0xe5, 0x55, 0xbb, - 0xa1, 0xe6, 0x7a, 0x40, 0xab, 0x36, 0xc3, 0x0d, 0xc8, 0x3a, 0x5e, 0xcb, 0x34, 0xd8, 0x86, 0x80, - 0x4d, 0x1f, 0x19, 0x16, 0x42, 0x18, 0x1f, 0xf4, 0x24, 0x8c, 0x78, 0xfe, 0x36, 0x6d, 0x32, 0xb1, - 0xf3, 0x8e, 0xaa, 0x69, 0x8f, 0xd1, 0xa6, 0xc9, 0xf0, 0xef, 0x10, 0xa4, 0xb9, 0xc9, 0xb4, 0x36, - 0x11, 0xfb, 0x67, 0x4e, 0xf9, 0x15, 0xda, 0x55, 0x86, 0x3f, 0x49, 0x49, 0xcf, 0xaf, 0x7c, 0xf1, - 0x62, 0xf2, 0x27, 0xa8, 0xb0, 0x72, 0x84, 0xa3, 0xa7, 0xf8, 0xd7, 0x31, 0xbd, 0x8e, 0x61, 0x95, - 0x57, 0xe8, 0xf6, 0x12, 0x7d, 0xac, 0xec, 0x70, 0xca, 0xae, 0x9a, 0xa4, 0x53, 0xbc, 0xf6, 0x9a, - 0x78, 0xd7, 0x28, 0x17, 0x60, 0xea, 0x30, 0x37, 0xd9, 0x3c, 0xc1, 0x7f, 0x46, 0x30, 0x21, 0x02, - 0x08, 0x16, 0xbf, 0x4d, 0x5d, 0x2e, 0x76, 0xed, 0xaf, 0x50, 0x24, 0x63, 0x7e, 0x24, 0x82, 0xfe, - 0x3c, 0x75, 0x39, 0xfe, 0x13, 0x82, 0xf1, 0x44, 0x44, 0x9b, 0x74, 0x47, 0xf4, 0x8d, 0xaf, 0x50, - 0x40, 0xb9, 0x5e, 0x40, 0xd7, 0xe9, 0x0e, 0xfe, 0x08, 0x46, 0x36, 0x28, 0xd1, 0xa9, 0xcb, 0xa4, - 0xec, 0x54, 0xaa, 0x94, 0xad, 0x5e, 0x1a, 0xb0, 0x98, 0x97, 0x02, 0xef, 0x45, 0x8b, 0xbb, 0x3b, - 0x6a, 0x84, 0x55, 0xa8, 0x41, 0x2e, 0xa9, 0xc0, 0x79, 0x48, 0xf9, 0xa9, 0x12, 0x5b, 0x84, 0xea, - 0x8b, 0xf8, 0x4d, 0x18, 0xde, 0x22, 0xa6, 0x47, 0x83, 0xed, 0x40, 0x0d, 0x06, 0xb5, 0xa1, 0x0f, - 0x51, 0x71, 0x01, 0x52, 0xab, 0x76, 0x03, 0xe7, 0x21, 0x57, 0x6f, 0x6a, 0x37, 0x6f, 0x35, 0x9a, - 0xda, 0xad, 0x95, 0xf9, 0xc5, 0xfc, 0x31, 0x7c, 0x02, 0xc6, 0xea, 0x4d, 0xed, 0xc6, 0x62, 0x3d, - 0xfa, 0x84, 0x7c, 0xa3, 0xc5, 0xef, 0xd6, 0xe7, 0x9b, 0x37, 0xee, 0x04, 0x5f, 0x86, 0x0a, 0xe9, - 0x7f, 0xbe, 0x98, 0x1c, 0x92, 0x50, 0x62, 0xdb, 0xfa, 0x1e, 0xc0, 0xf8, 0xde, 0x33, 0x12, 0xfe, - 0xe9, 0x10, 0xa4, 0x5d, 0xda, 0x31, 0x6c, 0x2b, 0xdc, 0xb5, 0x3e, 0x1d, 0xda, 0x55, 0xfe, 0x8b, - 0xdc, 0xff, 0x20, 0x15, 0xc8, 0xba, 0xcc, 0x6c, 0x8f, 0x6f, 0xc8, 0x17, 0xd4, 0x0c, 0x71, 0x64, - 0x4a, 0x18, 0x97, 0x2f, 0xf8, 0x07, 0x77, 0xd9, 0xb2, 0x5d, 0xbe, 0xf1, 0xd2, 0x71, 0x55, 0x05, - 0xe2, 0xf4, 0xdc, 0xc6, 0x23, 0x39, 0x61, 0x1b, 0x8f, 0xab, 0x6a, 0xae, 0x4d, 0xe4, 0x36, 0xb5, - 0xb8, 0x4b, 0x4c, 0xf9, 0x82, 0x9a, 0xa3, 0x5e, 0x62, 0x04, 0xd4, 0x0b, 0x70, 0x43, 0xb9, 0x47, - 0x85, 0x7a, 0xf2, 0x36, 0x15, 0x70, 0x3d, 0xb1, 0x1a, 0x8b, 0x73, 0x2a, 0x74, 0x69, 0x6c, 0xcc, - 0x48, 0xc4, 0x3b, 0xe3, 0xb1, 0x3e, 0xb1, 0x2a, 0xc4, 0x08, 0x2d, 0x12, 0xab, 0x6a, 0x98, 0x12, - 0x7c, 0x07, 0xc0, 0x3f, 0x12, 0x31, 0x26, 0xca, 0x3b, 0x38, 0xdb, 0xd7, 0x06, 0x3d, 0x87, 0x96, - 0xeb, 0x02, 0xe2, 0x3a, 0xdd, 0x51, 0x33, 0x24, 0x12, 0xf1, 0x1a, 0x64, 0x09, 0x63, 0x5e, 0x97, - 0x6a, 0xae, 0x6d, 0xd2, 0xf0, 0xc8, 0x7f, 0x69, 0x70, 0x6c, 0x81, 0xa1, 0xda, 0x26, 0x55, 0x81, - 0xf4, 0x64, 0xfc, 0x4b, 0x04, 0x79, 0x6a, 0xe9, 0x8e, 0x6d, 0x58, 0x5c, 0x23, 0xba, 0xee, 0x52, - 0xc6, 0xc2, 0x26, 0xf1, 0x78, 0x57, 0xf1, 0x5c, 0x26, 0x3d, 0x47, 0x55, 0xeb, 0x41, 0xa9, 0x54, - 0xf2, 0xaf, 0x02, 0x75, 0xf9, 0xae, 0x7f, 0x1b, 0x78, 0x9a, 0x90, 0x63, 0x71, 0x4d, 0xbe, 0x3f, - 0x9b, 0x50, 0xcc, 0xac, 0x95, 0x67, 0x66, 0x4b, 0xf7, 0xea, 0xf2, 0xdd, 0xf0, 0x0e, 0xf1, 0x34, - 0x21, 0xc7, 0xa2, 0xf0, 0x8a, 0x15, 0x33, 0x4f, 0x67, 0xa6, 0xd5, 0x89, 0x88, 0x51, 0x3d, 0x20, - 0x84, 0x35, 0x18, 0xd1, 0xe9, 0x3a, 0xf1, 0x4c, 0x2e, 0xda, 0x52, 0xb6, 0x3a, 0x3f, 0x70, 0xfc, - 0x0b, 0x81, 0xff, 0xb2, 0xc5, 0x69, 0x27, 0xb8, 0x50, 0x2f, 0x1d, 0x53, 0x23, 0xd4, 0xc2, 0x6f, - 0x10, 0x64, 0x7a, 0xd9, 0xc7, 0xdf, 0x82, 0xb1, 0x78, 0x35, 0xfd, 0xfe, 0x1b, 0x54, 0xfc, 0x1b, - 0xbb, 0x4a, 0xde, 0x1d, 0xcf, 0xe7, 0xfd, 0x94, 0x8c, 0x3c, 0xb8, 0xb7, 0xb6, 0x7d, 0x7f, 0x76, - 0x5a, 0xcd, 0xf6, 0x56, 0x6a, 0x59, 0xc7, 0x73, 0x70, 0x82, 0xd1, 0xb6, 0x4b, 0xb9, 0xb6, 0xaf, - 0x1a, 0x7a, 0x2d, 0xb7, 0xa4, 0x4e, 0x04, 0x16, 0xf1, 0x6c, 0x32, 0x8c, 0x31, 0xca, 0x98, 0x7f, - 0x0e, 0xe2, 0xf6, 0x26, 0xb5, 0xc2, 0x46, 0x1e, 0x9c, 0x0a, 0xa4, 0xe7, 0x43, 0x6a, 0x2e, 0x54, - 0x37, 0x7d, 0x6d, 0xe2, 0x57, 0xfa, 0x77, 0x04, 0x10, 0x2f, 0x2b, 0xbe, 0x0e, 0x29, 0xe2, 0x46, - 0xbf, 0xce, 0x8b, 0xbb, 0xca, 0x07, 0xee, 0x37, 0xab, 0xd5, 0x07, 0xc4, 0xb5, 0x6a, 0x64, 0x9b, - 0xd5, 0x0c, 0xd2, 0xad, 0xd5, 0xee, 0xf9, 0x29, 0x7e, 0x72, 0xa1, 0xfa, 0xac, 0xe6, 0xd7, 0xd2, - 0x5a, 0x25, 0x4e, 0xbc, 0x76, 0xf6, 0xdb, 0xe7, 0xca, 0x57, 0xe4, 0xfb, 0x67, 0xa7, 0x55, 0x1f, - 0x05, 0x5f, 0x82, 0x2c, 0x7d, 0xcc, 0xfd, 0x03, 0x84, 0x19, 0x1f, 0x40, 0x0a, 0xbb, 0xca, 0x49, - 0xf7, 0x6b, 0xd2, 0x1f, 0x32, 0xd5, 0xbc, 0x1f, 0xbe, 0x70, 0xa9, 0xad, 0x55, 0x64, 0x3f, 0x0f, - 0x10, 0x99, 0x2f, 0xeb, 0x78, 0x01, 0xf2, 0x51, 0x44, 0xd1, 0xeb, 0x45, 0x58, 0xb7, 0x93, 0x7d, - 0x57, 0xd5, 0x85, 0xd0, 0xc0, 0xcf, 0x8b, 0x70, 0x89, 0x3e, 0x24, 0x02, 0x25, 0x80, 0xfb, 0x97, - 0x0f, 0xd7, 0x01, 0xc4, 0x1b, 0x85, 0x26, 0x4e, 0x3f, 0x41, 0xd8, 0xc5, 0x5d, 0xe5, 0x6d, 0xf7, - 0xb4, 0xbf, 0x40, 0xd2, 0x83, 0x30, 0xb8, 0x7d, 0xb5, 0x36, 0xad, 0x66, 0x84, 0xd7, 0x0a, 0xe9, - 0xd2, 0x78, 0x8a, 0x58, 0x52, 0x72, 0x00, 0x3a, 0x75, 0x4c, 0x7b, 0xa7, 0x4b, 0x2d, 0x5e, 0xa8, - 0xc2, 0x48, 0x74, 0xf7, 0x39, 0x0d, 0xc3, 0xc1, 0xb5, 0x10, 0xed, 0x3d, 0x43, 0x05, 0x5f, 0x5f, - 0x8a, 0x35, 0x01, 0xa3, 0x4e, 0xb4, 0x81, 0xa6, 0xfe, 0xad, 0xa0, 0xe2, 0x2a, 0xe0, 0xbe, 0x4a, - 0x65, 0xf8, 0x12, 0x8c, 0x04, 0xcf, 0x65, 0x4c, 0x42, 0xa2, 0xa3, 0x7c, 0xe3, 0xc0, 0xf2, 0x56, - 0x23, 0x8f, 0xe2, 0xaf, 0x11, 0x48, 0x7d, 0xea, 0xab, 0xe2, 0x9e, 0xce, 0xf0, 0x2d, 0x18, 0x09, - 0xae, 0xec, 0x11, 0xf2, 0xfb, 0x07, 0x22, 0x87, 0xae, 0xe5, 0xf0, 0xff, 0xb0, 0x4b, 0x85, 0x28, - 0x7e, 0x97, 0x4a, 0x2a, 0x06, 0xea, 0x52, 0xbf, 0x40, 0x70, 0xea, 0x1a, 0xe5, 0xfd, 0xb1, 0xd0, - 0x47, 0x1e, 0x65, 0x1c, 0x2f, 0x1d, 0xfd, 0xc9, 0x25, 0x7e, 0x2e, 0x12, 0x4f, 0x2d, 0x17, 0x01, - 0xe2, 0x97, 0xaf, 0x2f, 0x7d, 0x6a, 0xb9, 0xea, 0x9b, 0xdc, 0x24, 0x6c, 0x53, 0xcd, 0xac, 0x47, - 0x62, 0xf1, 0x73, 0x04, 0xa7, 0x6f, 0x18, 0xac, 0x9f, 0x25, 0x8b, 0x68, 0xfe, 0x1f, 0xdf, 0xb6, - 0x8e, 0xce, 0x3b, 0xf1, 0x9e, 0xf5, 0x33, 0x04, 0xa7, 0x1a, 0xaf, 0x48, 0xf3, 0x3c, 0xa4, 0x83, - 0xda, 0x09, 0x69, 0x1f, 0x5c, 0x6c, 0x09, 0xc6, 0xa1, 0xeb, 0x6b, 0x30, 0xad, 0x7e, 0x9e, 0x86, - 0xc9, 0x97, 0x90, 0xeb, 0x18, 0xcc, 0x2f, 0xa8, 0x87, 0x00, 0xd7, 0x28, 0x8f, 0xea, 0xf7, 0xeb, - 0x7d, 0x90, 0x8b, 0x5d, 0x87, 0xef, 0x14, 0x4a, 0x87, 0x2d, 0xe3, 0x62, 0xe1, 0xd3, 0xbf, 0xfc, - 0xed, 0x47, 0x43, 0x6f, 0x62, 0x5c, 0x21, 0xac, 0x12, 0x90, 0x97, 0xc3, 0x62, 0xc6, 0x3f, 0x47, - 0x90, 0xba, 0x46, 0x39, 0x3e, 0xbb, 0x1f, 0xed, 0x15, 0x55, 0x5a, 0x38, 0x38, 0x5d, 0xc5, 0x25, - 0x31, 0xa7, 0x82, 0xaf, 0xc4, 0x73, 0x56, 0x9e, 0x18, 0x3a, 0x2b, 0xef, 0xab, 0x9b, 0x7d, 0xe3, - 0x67, 0x81, 0x51, 0xfc, 0xba, 0xf9, 0x0c, 0xff, 0x10, 0xc1, 0x71, 0xbf, 0x1a, 0xb1, 0xbc, 0x7f, - 0xd6, 0x57, 0xd6, 0x68, 0xa1, 0x78, 0x20, 0x49, 0x56, 0x9c, 0x13, 0x2c, 0x65, 0x7c, 0x36, 0xc9, - 0xf2, 0x00, 0x86, 0xf8, 0x5f, 0x08, 0x52, 0x8d, 0x97, 0xa5, 0xac, 0xf1, 0x7a, 0x29, 0xfb, 0x31, - 0x12, 0x6c, 0x3e, 0x43, 0x85, 0x95, 0x24, 0x9d, 0xf0, 0xaf, 0x08, 0x87, 0xca, 0x5d, 0xc2, 0x36, - 0x91, 0xc2, 0x1a, 0x9a, 0xbd, 0x7b, 0xa9, 0xf8, 0xc1, 0xd1, 0x40, 0x6b, 0x68, 0x16, 0x7f, 0x86, - 0x20, 0xbd, 0x40, 0x4d, 0xca, 0x29, 0x1e, 0x68, 0x4f, 0x2a, 0x7c, 0x49, 0xed, 0x16, 0xaf, 0x88, - 0x48, 0x6b, 0xb3, 0x1f, 0x0e, 0x90, 0x77, 0x41, 0x3a, 0x0a, 0x49, 0x79, 0xff, 0xf7, 0x7f, 0x3d, - 0x83, 0xee, 0x56, 0x3a, 0x76, 0x99, 0x6f, 0x50, 0x2e, 0xae, 0x35, 0x65, 0x8b, 0xf2, 0x6d, 0xdb, - 0xdd, 0xac, 0xec, 0x7d, 0xbe, 0xdf, 0x9a, 0xab, 0x38, 0x9b, 0x9d, 0x0a, 0xe7, 0x96, 0xd3, 0x6a, - 0xa5, 0x05, 0x91, 0xb9, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x96, 0x7e, 0x6c, 0x88, 0xc0, 0x19, - 0x00, 0x00, + // 2217 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x59, 0xcf, 0x6f, 0xdb, 0xc8, + 0x15, 0xce, 0x58, 0xb1, 0x6c, 0x3d, 0xc9, 0xb6, 0x32, 0x9b, 0x76, 0x69, 0x65, 0x93, 0xcd, 0x6a, + 0x8d, 0x5d, 0xd9, 0x09, 0xa5, 0x44, 0xee, 0x6e, 0x37, 0x0a, 0x8a, 0x44, 0xb4, 0x9d, 0xd8, 0x4d, + 0xe2, 0xc4, 0x94, 0xb6, 0xdb, 0x24, 0x4e, 0x88, 0x91, 0x38, 0x96, 0x19, 0x53, 0x24, 0xc3, 0x19, + 0xda, 0x71, 0x7e, 0x00, 0xc1, 0x5e, 0x5a, 0xf4, 0x50, 0x2c, 0xd0, 0x43, 0x51, 0x14, 0x45, 0x81, + 0x16, 0x68, 0x81, 0x1c, 0x8b, 0x9e, 0xbb, 0xed, 0xad, 0x28, 0x50, 0xf4, 0xd0, 0x3f, 0xa1, 0x45, + 0x0f, 0xbd, 0x14, 0xd8, 0x53, 0xe1, 0x02, 0x45, 0xc1, 0x21, 0x29, 0xd2, 0x56, 0x36, 0xb6, 0x1c, + 0xf4, 0xb0, 0x97, 0xe4, 0x0d, 0xdf, 0x7b, 0xdf, 0x7c, 0xef, 0xcd, 0xf3, 0xbc, 0x99, 0x11, 0x9c, + 0x33, 0x6d, 0x97, 0x6c, 0x11, 0x4b, 0x66, 0x9c, 0xb4, 0x37, 0x2a, 0xc4, 0x31, 0x2a, 0xc4, 0x71, + 0x4c, 0xa3, 0x4d, 0xb8, 0x61, 0x5b, 0x8c, 0xba, 0x9b, 0xd4, 0xd5, 0x1c, 0xaf, 0xc5, 0xbc, 0x56, + 0xd9, 0x71, 0x6d, 0x6e, 0xe3, 0x71, 0xce, 0xad, 0x72, 0xe8, 0x55, 0xde, 0x9c, 0x2d, 0xcc, 0x77, + 0x0c, 0xbe, 0xee, 0xb5, 0xca, 0x6d, 0xbb, 0x5b, 0x69, 0xae, 0xd3, 0xe6, 0xba, 0x61, 0x75, 0xd8, + 0x92, 0xa5, 0x7b, 0x8c, 0xbb, 0x06, 0x65, 0x15, 0xe1, 0xd5, 0x96, 0x3b, 0xd4, 0x92, 0x3b, 0xb6, + 0xbc, 0x66, 0x92, 0x0e, 0xab, 0x10, 0xcb, 0xb2, 0x79, 0x30, 0x43, 0x80, 0x5a, 0xa8, 0x27, 0x50, + 0xa8, 0xb5, 0x69, 0x6f, 0x3b, 0xae, 0xfd, 0x68, 0x3b, 0xe9, 0xbc, 0x49, 0x4c, 0x43, 0x27, 0x9c, + 0x56, 0xfa, 0x84, 0x10, 0x42, 0x4e, 0x40, 0x74, 0xec, 0x8e, 0x1d, 0x38, 0xb7, 0xbc, 0x35, 0x31, + 0x12, 0x03, 0x21, 0x85, 0xe6, 0x73, 0x03, 0xf1, 0x7e, 0xc0, 0x6c, 0xeb, 0x25, 0xb4, 0xdf, 0xea, + 0xd8, 0x76, 0xc7, 0xa4, 0x41, 0xde, 0xfa, 0xb4, 0xa7, 0x42, 0x6d, 0x8f, 0x88, 0xee, 0xb9, 0xc2, + 0x20, 0xd4, 0x9f, 0xd8, 0xab, 0xa7, 0x5d, 0x87, 0x6f, 0x87, 0xca, 0xd3, 0x7b, 0x95, 0x6b, 0x06, + 0x35, 0x75, 0xad, 0x4b, 0xd8, 0x46, 0x68, 0xf1, 0xf6, 0x5e, 0x0b, 0x6e, 0x74, 0x29, 0xe3, 0xa4, + 0xeb, 0x84, 0x06, 0xef, 0xf6, 0x2f, 0xae, 0xa1, 0x53, 0x8b, 0x1b, 0x6b, 0x06, 0x75, 0x43, 0x92, + 0xc5, 0x3f, 0x21, 0x78, 0xab, 0x1e, 0x2f, 0xf9, 0x2d, 0xaf, 0xd5, 0xf0, 0x5a, 0x4b, 0xb1, 0x19, + 0xbe, 0x0d, 0x13, 0x89, 0x92, 0xd0, 0x0c, 0x9d, 0x49, 0xe8, 0x34, 0x2a, 0x65, 0xab, 0xef, 0x95, + 0x77, 0x97, 0x42, 0x39, 0x01, 0x93, 0x00, 0x50, 0x46, 0x77, 0x94, 0xe1, 0x1f, 0xa0, 0xa1, 0x3c, + 0x52, 0xc7, 0x49, 0xd2, 0x82, 0xe1, 0x05, 0x00, 0xc7, 0x6b, 0x69, 0xcc, 0x6b, 0x69, 0x86, 0x2e, + 0x0d, 0x9d, 0x46, 0xa5, 0x8c, 0xf2, 0xfe, 0x8e, 0x32, 0xe5, 0x16, 0xa5, 0xa9, 0xea, 0xa9, 0xfb, + 0x77, 0x89, 0xfc, 0xf8, 0x9c, 0x7c, 0xe1, 0x5e, 0xe9, 0x52, 0xed, 0xae, 0x7c, 0xef, 0x52, 0x34, + 0x9c, 0x7e, 0x52, 0x3d, 0xfb, 0x6c, 0x4a, 0x1d, 0x75, 0x42, 0xaa, 0xb5, 0xd1, 0x2f, 0x5e, 0x4c, + 0x1e, 0x1d, 0x3d, 0x92, 0x47, 0xc5, 0x3f, 0xbc, 0x03, 0xc7, 0xfa, 0x82, 0xc1, 0xb7, 0x20, 0x15, + 0xb3, 0x3e, 0xfb, 0x0a, 0xd6, 0x7d, 0xc1, 0x2b, 0xf9, 0x88, 0x7b, 0x30, 0x45, 0x09, 0xa9, 0x3e, + 0x14, 0x9e, 0x03, 0x68, 0xbb, 0x94, 0x70, 0xaa, 0x6b, 0x84, 0x0b, 0xe2, 0xd9, 0x6a, 0xa1, 0x1c, + 0xac, 0x47, 0x39, 0x5a, 0x8f, 0x72, 0x33, 0x5a, 0x0f, 0x25, 0x62, 0x78, 0x44, 0xcd, 0x84, 0x7e, + 0x75, 0xee, 0x83, 0x78, 0x8e, 0x1e, 0x81, 0xa4, 0x06, 0x01, 0x09, 0xfd, 0xea, 0x1c, 0x5f, 0x82, + 0xf4, 0x9a, 0xed, 0x76, 0x09, 0x97, 0x8e, 0x26, 0xd3, 0x77, 0x7c, 0xdf, 0xf4, 0x85, 0x6e, 0x78, + 0x1e, 0x8e, 0x5a, 0x84, 0x33, 0xe9, 0x98, 0x98, 0xbf, 0xbc, 0x6f, 0x76, 0xca, 0xcb, 0xf5, 0x66, + 0xe3, 0x96, 0x6b, 0x6f, 0x1a, 0x3a, 0x75, 0x17, 0x8f, 0xa8, 0xc2, 0xdb, 0x47, 0xe9, 0x3e, 0xe4, + 0x5c, 0x9a, 0x3c, 0x28, 0xca, 0x8d, 0x95, 0x66, 0x33, 0x89, 0xe2, 0x7b, 0xe3, 0x6b, 0x30, 0x42, + 0xb6, 0x98, 0x66, 0xd8, 0x5c, 0xa2, 0x02, 0xe8, 0xdc, 0xfe, 0x40, 0xf5, 0x4f, 0x1a, 0x4b, 0x76, + 0x12, 0x2a, 0x4d, 0xb6, 0xd8, 0x92, 0xcd, 0xf1, 0x7b, 0x00, 0x2d, 0xc2, 0xa8, 0xc6, 0x6d, 0xc7, + 0x68, 0x4b, 0x69, 0x91, 0x9d, 0x91, 0x1d, 0xe5, 0xa8, 0x3b, 0x24, 0xe9, 0x6a, 0xc6, 0x57, 0x35, + 0x7d, 0x0d, 0x5e, 0x86, 0x31, 0xdd, 0xde, 0xb2, 0x4c, 0xc3, 0xda, 0xd0, 0x1c, 0x8f, 0xad, 0x4b, + 0x23, 0x62, 0xea, 0xe9, 0x03, 0xc4, 0x40, 0x19, 0x23, 0x1d, 0xaa, 0xe6, 0x22, 0xff, 0x5b, 0x1e, + 0x5b, 0xc7, 0x4d, 0xc8, 0xf7, 0xf0, 0x5c, 0xea, 0x98, 0xa4, 0x4d, 0xa5, 0xd1, 0x41, 0x21, 0x27, + 0x22, 0x08, 0x35, 0x40, 0xc0, 0xb7, 0x60, 0xdc, 0x73, 0x04, 0x66, 0x37, 0x30, 0x91, 0x32, 0x83, + 0x62, 0x8e, 0x05, 0x00, 0xe1, 0x10, 0x7f, 0x07, 0x8e, 0x85, 0x88, 0x96, 0x5f, 0x09, 0xa6, 0xf1, + 0x98, 0xea, 0xd2, 0xf1, 0x41, 0x41, 0xf3, 0x01, 0xc6, 0x72, 0x0f, 0x02, 0x7f, 0x1b, 0xb2, 0x0f, + 0x6c, 0xc3, 0xd2, 0x48, 0xbb, 0x4d, 0x1d, 0x2e, 0xc1, 0xa0, 0x88, 0xe0, 0x7b, 0xd7, 0x85, 0x33, + 0xbe, 0x0e, 0xbd, 0xdc, 0x6a, 0xa4, 0xbd, 0x21, 0x65, 0x07, 0x05, 0xcb, 0x46, 0xee, 0xf5, 0xf6, + 0xc6, 0xae, 0x95, 0xb6, 0x7c, 0xb8, 0xdc, 0xa1, 0x57, 0x7a, 0x99, 0xec, 0xc1, 0x63, 0xd4, 0xe2, + 0xd2, 0xd8, 0xa1, 0xf1, 0x1a, 0xd4, 0xe2, 0x58, 0x85, 0xde, 0xb2, 0x6b, 0x6b, 0xc4, 0x30, 0xa9, + 0x2e, 0x8d, 0x0f, 0x8a, 0x38, 0x1e, 0x21, 0x5c, 0x11, 0x00, 0xbb, 0x30, 0x1f, 0x7a, 0xd4, 0xa3, + 0xba, 0x34, 0x71, 0x68, 0xcc, 0x15, 0x01, 0x80, 0x3b, 0x50, 0xd8, 0x8d, 0xa9, 0x19, 0x56, 0xd4, + 0x8c, 0x75, 0xe9, 0x8d, 0x41, 0xe1, 0xa5, 0x5d, 0xf0, 0x4b, 0x31, 0x94, 0x4f, 0xde, 0xb4, 0xc3, + 0xbe, 0xc3, 0x6c, 0x73, 0x93, 0xea, 0x52, 0x7e, 0x60, 0xf2, 0x11, 0x42, 0x43, 0x00, 0xf8, 0x25, + 0xe5, 0x1f, 0x6b, 0x8c, 0x36, 0xd5, 0x74, 0xc2, 0x89, 0x84, 0x07, 0x2e, 0xa9, 0xd0, 0x7d, 0x9e, + 0x70, 0x52, 0xa8, 0x43, 0x2e, 0xb9, 0x1f, 0xe2, 0xf7, 0x01, 0xc2, 0x43, 0x93, 0xe7, 0x9a, 0xa2, + 0xe3, 0x64, 0x44, 0xff, 0x73, 0x53, 0xdf, 0x47, 0x48, 0xcd, 0x04, 0xba, 0x8f, 0x5d, 0x33, 0xec, + 0x59, 0x28, 0x8f, 0x0a, 0xbf, 0xcd, 0x40, 0x2e, 0xb9, 0x1b, 0x1e, 0x18, 0x03, 0x4f, 0x41, 0xa6, + 0x6d, 0x1a, 0xd4, 0xe2, 0x71, 0xf7, 0x0c, 0x37, 0xb8, 0x37, 0xd5, 0xd1, 0x40, 0xb3, 0xa4, 0xe3, + 0x77, 0x61, 0xd4, 0x63, 0xd4, 0xb5, 0x48, 0x97, 0x8a, 0x26, 0x93, 0xd8, 0x05, 0x7b, 0x0a, 0xdf, + 0xc8, 0x21, 0x8c, 0x6d, 0xd9, 0xae, 0x1e, 0x36, 0x92, 0xd8, 0x28, 0x52, 0xe0, 0x4f, 0x60, 0x8c, + 0x79, 0x2d, 0xd6, 0x76, 0x8d, 0x16, 0xd5, 0x1e, 0xda, 0x4c, 0x1a, 0x3e, 0x8d, 0x4a, 0xe3, 0xd5, + 0xea, 0x60, 0xbb, 0x7d, 0x79, 0xc5, 0x6e, 0xa8, 0xb9, 0x1e, 0xd0, 0x8a, 0xcd, 0x70, 0x03, 0xb2, + 0x8e, 0xd7, 0x32, 0x0d, 0xb6, 0x2e, 0x60, 0xd3, 0x87, 0x86, 0x85, 0x10, 0xc6, 0x07, 0x7d, 0x13, + 0x46, 0x3c, 0x7f, 0xfb, 0x37, 0x99, 0xd8, 0xd1, 0x47, 0xd5, 0xb4, 0xc7, 0x68, 0xd3, 0x64, 0xf8, + 0x77, 0x08, 0xd2, 0xdc, 0x64, 0x5a, 0x9b, 0x88, 0x7d, 0x39, 0xa7, 0xfc, 0x0a, 0xed, 0x28, 0xc3, + 0x8f, 0x53, 0xd2, 0xf3, 0xcb, 0x5f, 0xbc, 0x98, 0xfc, 0x09, 0x2a, 0x2c, 0x1f, 0xe2, 0x48, 0x2b, + 0xfe, 0x75, 0x4c, 0xaf, 0x63, 0x58, 0xe5, 0x65, 0xba, 0xb5, 0x48, 0x1f, 0x29, 0xdb, 0x9c, 0xb2, + 0x2b, 0x26, 0xe9, 0x14, 0xaf, 0xbe, 0x26, 0xde, 0x55, 0xca, 0x05, 0x98, 0x3a, 0xcc, 0x4d, 0x36, + 0x47, 0xf0, 0x5f, 0x10, 0x4c, 0x88, 0x00, 0x82, 0xc5, 0x6f, 0x53, 0x97, 0x8b, 0x6e, 0xf0, 0x15, + 0x8a, 0x64, 0xcc, 0x8f, 0x44, 0xd0, 0x9f, 0xa3, 0x2e, 0xc7, 0x7f, 0x46, 0x30, 0x9e, 0x88, 0x68, + 0x83, 0x6e, 0x8b, 0xbe, 0xf1, 0x15, 0x0a, 0x28, 0xd7, 0x0b, 0xe8, 0x1a, 0xdd, 0xc6, 0x1f, 0xc3, + 0xc8, 0x3a, 0x25, 0x3a, 0x75, 0x99, 0x94, 0x3d, 0x9d, 0x2a, 0x65, 0xab, 0x17, 0x07, 0x2c, 0xe6, + 0xc5, 0xc0, 0x7b, 0xc1, 0xe2, 0xee, 0xb6, 0x1a, 0x61, 0x15, 0x6a, 0x90, 0x4b, 0x2a, 0x70, 0x1e, + 0x52, 0x7e, 0xaa, 0xc4, 0x16, 0xa1, 0xfa, 0x22, 0x3e, 0x0e, 0xc3, 0x9b, 0xc4, 0xf4, 0x68, 0xb0, + 0x1d, 0xa8, 0xc1, 0xa0, 0x36, 0xf4, 0x11, 0x2a, 0xce, 0x43, 0x6a, 0xc5, 0x6e, 0xe0, 0x3c, 0xe4, + 0xea, 0x4d, 0xed, 0xc6, 0xcd, 0x46, 0x53, 0xbb, 0xb9, 0x3c, 0xb7, 0x90, 0x3f, 0x82, 0x8f, 0xc1, + 0x58, 0xbd, 0xa9, 0x5d, 0x5f, 0xa8, 0x47, 0x9f, 0x90, 0x6f, 0xb4, 0xf0, 0xdd, 0xfa, 0x5c, 0xf3, + 0xfa, 0xed, 0xe0, 0xcb, 0x50, 0x21, 0xfd, 0xcf, 0x17, 0x93, 0x43, 0x12, 0x4a, 0x6c, 0x5b, 0xdf, + 0x03, 0x18, 0xdf, 0x7d, 0xf6, 0xc2, 0x3f, 0x1d, 0x82, 0xb4, 0x4b, 0x3b, 0x86, 0x6d, 0x85, 0xbb, + 0xd6, 0xa7, 0x43, 0x3b, 0xca, 0x7f, 0x91, 0xfb, 0x1f, 0xa4, 0x02, 0x59, 0x93, 0x99, 0xed, 0xf1, + 0x75, 0xf9, 0xbc, 0x9a, 0x21, 0x8e, 0x4c, 0x09, 0xe3, 0xf2, 0x79, 0xff, 0x42, 0x20, 0x5b, 0xb6, + 0xcb, 0xd7, 0x5f, 0x3a, 0xae, 0xaa, 0x40, 0x9c, 0x9e, 0xdb, 0x78, 0x24, 0x27, 0x6c, 0xe3, 0x71, + 0x55, 0xcd, 0xb5, 0x89, 0xdc, 0xa6, 0x16, 0x77, 0x89, 0x29, 0x9f, 0x57, 0x73, 0xd4, 0x4b, 0x8c, + 0x80, 0x7a, 0x01, 0x6e, 0x28, 0xf7, 0xa8, 0x50, 0x4f, 0xde, 0xa2, 0x02, 0xae, 0x27, 0x56, 0x63, + 0x71, 0x56, 0x85, 0x2e, 0x8d, 0x8d, 0x19, 0x89, 0x78, 0x67, 0x3c, 0xd6, 0x27, 0x56, 0x85, 0x18, + 0xa1, 0x45, 0x62, 0x55, 0x0d, 0x53, 0x82, 0x6f, 0x03, 0xf8, 0x47, 0x22, 0xc6, 0x44, 0x79, 0x07, + 0x77, 0x86, 0xda, 0xa0, 0xe7, 0xdb, 0x72, 0x5d, 0x40, 0x5c, 0xa3, 0xdb, 0x6a, 0x86, 0x44, 0x22, + 0x5e, 0x85, 0x2c, 0x61, 0xcc, 0xeb, 0x52, 0xcd, 0xb5, 0x4d, 0x1a, 0x5e, 0x25, 0x2e, 0x0e, 0x8e, + 0x2d, 0x30, 0x54, 0xdb, 0xa4, 0x2a, 0x90, 0x9e, 0x8c, 0x7f, 0x89, 0x20, 0x4f, 0x2d, 0xdd, 0xb1, + 0x0d, 0x8b, 0x6b, 0x44, 0xd7, 0x5d, 0xca, 0x58, 0xd8, 0x24, 0x1e, 0xed, 0x28, 0x9e, 0xcb, 0xa4, + 0xe7, 0xa8, 0x6a, 0xdd, 0x2f, 0x95, 0x4a, 0xfe, 0x15, 0xa3, 0x2e, 0xdf, 0xf1, 0x6f, 0x19, 0x4f, + 0x13, 0x72, 0x2c, 0xae, 0xca, 0xf7, 0x66, 0x12, 0x8a, 0xe9, 0xd5, 0xf2, 0xf4, 0x4c, 0xe9, 0x6e, + 0x5d, 0xbe, 0x13, 0xde, 0x4d, 0x9e, 0x26, 0xe4, 0x58, 0x14, 0x5e, 0xb1, 0x62, 0xfa, 0xe9, 0xf4, + 0x94, 0x3a, 0x11, 0x31, 0xaa, 0x07, 0x84, 0xb0, 0x06, 0x23, 0x3a, 0x5d, 0x23, 0x9e, 0xc9, 0x45, + 0x5b, 0xca, 0x56, 0xe7, 0x06, 0x8e, 0x7f, 0x3e, 0xf0, 0x5f, 0xb2, 0x38, 0xed, 0x04, 0x17, 0xf5, + 0xc5, 0x23, 0x6a, 0x84, 0x5a, 0xf8, 0x0d, 0x82, 0x4c, 0x2f, 0xfb, 0xf8, 0x9b, 0x30, 0x16, 0xaf, + 0xa6, 0xdf, 0x7f, 0x83, 0x8a, 0x7f, 0x63, 0x47, 0xc9, 0xbb, 0xe3, 0xf9, 0xbc, 0x9f, 0x92, 0x91, + 0xfb, 0x77, 0x57, 0xb7, 0xee, 0xcd, 0x4c, 0xa9, 0xd9, 0xde, 0x4a, 0x2d, 0xe9, 0x78, 0x16, 0x8e, + 0x31, 0xda, 0x76, 0x29, 0xd7, 0xf6, 0x54, 0x43, 0xaf, 0xe5, 0x96, 0xd4, 0x89, 0xc0, 0x22, 0x9e, + 0x4d, 0x86, 0x31, 0x46, 0x19, 0xf3, 0xcf, 0x41, 0xdc, 0xde, 0xa0, 0x56, 0xd8, 0xc8, 0x83, 0x53, + 0x81, 0xf4, 0x7c, 0x48, 0xcd, 0x85, 0xea, 0xa6, 0xaf, 0x4d, 0xfc, 0x95, 0xfe, 0x03, 0x01, 0xc4, + 0xcb, 0x8a, 0xaf, 0x41, 0x8a, 0xb8, 0xd1, 0x5f, 0xe7, 0x85, 0x1d, 0xe5, 0x43, 0xf7, 0x1b, 0xd5, + 0xea, 0x7d, 0xe2, 0x5a, 0x35, 0xb2, 0xc5, 0x6a, 0x06, 0xe9, 0xd6, 0x6a, 0x77, 0xfd, 0x14, 0x3f, + 0x39, 0x5f, 0x7d, 0x56, 0xf3, 0x6b, 0x69, 0xb5, 0x12, 0x27, 0x5e, 0x3b, 0xf3, 0xad, 0xb3, 0xe5, + 0xcb, 0xf2, 0xbd, 0x33, 0x53, 0xaa, 0x8f, 0x82, 0x2f, 0x42, 0x96, 0x3e, 0xe2, 0xfe, 0x01, 0xc2, + 0x8c, 0x0f, 0x20, 0x85, 0x1d, 0xe5, 0x4d, 0xf7, 0x6b, 0xd2, 0x1f, 0x33, 0xd5, 0xbc, 0x1f, 0xbe, + 0x70, 0xa9, 0xad, 0x56, 0x64, 0x3f, 0x0f, 0x10, 0x99, 0x2f, 0xe9, 0x78, 0x1e, 0xf2, 0x51, 0x44, + 0xd1, 0xab, 0x48, 0x58, 0xb7, 0x93, 0x7d, 0x57, 0xe0, 0xf9, 0xd0, 0xc0, 0xcf, 0x8b, 0x70, 0x89, + 0x3e, 0x24, 0x02, 0x25, 0x80, 0xfb, 0x97, 0x0f, 0xd7, 0x01, 0xc4, 0xdb, 0x87, 0x26, 0x4e, 0x3f, + 0x41, 0xd8, 0xc5, 0x1d, 0xe5, 0x6d, 0xf7, 0xa4, 0xbf, 0x40, 0xd2, 0xfd, 0x30, 0xb8, 0x3d, 0xb5, + 0x36, 0xa5, 0x66, 0x84, 0xd7, 0x32, 0xe9, 0xd2, 0x78, 0x8a, 0x58, 0x52, 0x72, 0x00, 0x3a, 0x75, + 0x4c, 0x7b, 0xbb, 0x4b, 0x2d, 0x5e, 0xa8, 0xc2, 0x48, 0x74, 0xa7, 0x3a, 0x09, 0xc3, 0xc1, 0x75, + 0x13, 0xed, 0x3e, 0x43, 0x05, 0x5f, 0x5f, 0x8a, 0x35, 0x01, 0xa3, 0x4e, 0xb4, 0x81, 0xa6, 0xfe, + 0xad, 0xa0, 0xe2, 0x0a, 0xe0, 0xbe, 0x4a, 0x65, 0xf8, 0x22, 0x8c, 0x04, 0xcf, 0x70, 0x4c, 0x42, + 0xa2, 0xa3, 0xbc, 0xb3, 0x6f, 0x79, 0xab, 0x91, 0x47, 0xf1, 0xd7, 0x08, 0xa4, 0x3e, 0xf5, 0x15, + 0x71, 0xff, 0x67, 0xf8, 0x26, 0x8c, 0x04, 0x4f, 0x01, 0x11, 0xf2, 0x07, 0xfb, 0x22, 0x87, 0xae, + 0xe5, 0xf0, 0xff, 0xb0, 0x4b, 0x85, 0x28, 0x7e, 0x97, 0x4a, 0x2a, 0x06, 0xea, 0x52, 0xbf, 0x40, + 0x70, 0xe2, 0x2a, 0xe5, 0xfd, 0xb1, 0xd0, 0x87, 0x1e, 0x65, 0x1c, 0x2f, 0x1e, 0xfe, 0x29, 0x27, + 0x7e, 0x86, 0x12, 0x4f, 0x38, 0x17, 0x00, 0xe2, 0x17, 0xb5, 0x2f, 0x7d, 0xc2, 0xb9, 0xe2, 0x9b, + 0xdc, 0x20, 0x6c, 0x43, 0xcd, 0xac, 0x45, 0x62, 0xf1, 0x73, 0x04, 0x27, 0xaf, 0x1b, 0xac, 0x9f, + 0x25, 0x8b, 0x68, 0xfe, 0x1f, 0xdf, 0xcc, 0x0e, 0xcf, 0x3b, 0xf1, 0x4e, 0xf6, 0x33, 0x04, 0x27, + 0x1a, 0xaf, 0x48, 0xf3, 0x1c, 0xa4, 0x83, 0xda, 0x09, 0x69, 0xef, 0x5f, 0x6c, 0x09, 0xc6, 0xa1, + 0xeb, 0x6b, 0x30, 0xad, 0x7e, 0x9e, 0x86, 0xc9, 0x97, 0x90, 0xeb, 0x18, 0xcc, 0x2f, 0xa8, 0x07, + 0x00, 0x57, 0x29, 0x8f, 0xea, 0xf7, 0xeb, 0x7d, 0x90, 0x0b, 0x5d, 0x87, 0x6f, 0x17, 0x4a, 0x07, + 0x2d, 0xe3, 0x62, 0xe1, 0xd3, 0xbf, 0xfe, 0xfd, 0x47, 0x43, 0xc7, 0x31, 0xae, 0x10, 0x56, 0x09, + 0xc8, 0xcb, 0x61, 0x31, 0xe3, 0x9f, 0x23, 0x48, 0x5d, 0xa5, 0x1c, 0x9f, 0xd9, 0x8b, 0xf6, 0x8a, + 0x2a, 0x2d, 0xec, 0x9f, 0xae, 0xe2, 0xa2, 0x98, 0x53, 0xc1, 0x97, 0xe3, 0x39, 0x2b, 0x4f, 0x0c, + 0x9d, 0x95, 0xf7, 0xd4, 0xcd, 0x9e, 0xf1, 0xb3, 0xc0, 0x28, 0x7e, 0x35, 0x7d, 0x86, 0x7f, 0x88, + 0xe0, 0xa8, 0x5f, 0x8d, 0x58, 0xde, 0x3b, 0xeb, 0x2b, 0x6b, 0xb4, 0x50, 0xdc, 0x97, 0x24, 0x2b, + 0xce, 0x0a, 0x96, 0x32, 0x3e, 0x93, 0x64, 0xb9, 0x0f, 0x43, 0xfc, 0x2f, 0x04, 0xa9, 0xc6, 0xcb, + 0x52, 0xd6, 0x78, 0xbd, 0x94, 0xfd, 0x18, 0x09, 0x36, 0x9f, 0xa1, 0xc2, 0x72, 0x92, 0x4e, 0xf8, + 0xeb, 0xc4, 0x81, 0x72, 0x97, 0xb0, 0x4d, 0xa4, 0xb0, 0x86, 0x66, 0xee, 0x5c, 0x2c, 0x7e, 0x78, + 0x38, 0xd0, 0x1a, 0x9a, 0xc1, 0x9f, 0x21, 0x48, 0xcf, 0x53, 0x93, 0x72, 0x8a, 0x07, 0xda, 0x93, + 0x0a, 0x5f, 0x52, 0xbb, 0xc5, 0xcb, 0x22, 0xd2, 0xda, 0xcc, 0x47, 0x03, 0xe4, 0x5d, 0x90, 0x8e, + 0x42, 0x52, 0x3e, 0xf8, 0xfd, 0xdf, 0x4e, 0xa1, 0x3b, 0x95, 0x8e, 0x5d, 0xe6, 0xeb, 0x94, 0x8b, + 0x6b, 0x4d, 0xd9, 0xa2, 0x7c, 0xcb, 0x76, 0x37, 0x2a, 0xbb, 0x7f, 0x16, 0xd8, 0x9c, 0xad, 0x38, + 0x1b, 0x9d, 0x0a, 0xe7, 0x96, 0xd3, 0x6a, 0xa5, 0x05, 0x91, 0xd9, 0xff, 0x05, 0x00, 0x00, 0xff, + 0xff, 0x08, 0xc8, 0x2f, 0x15, 0x18, 0x1a, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/pkg/ttnpb/applicationserver_pubsub.pb.paths.fm.go b/pkg/ttnpb/applicationserver_pubsub.pb.paths.fm.go index e703b97936..5d37b55266 100644 --- a/pkg/ttnpb/applicationserver_pubsub.pb.paths.fm.go +++ b/pkg/ttnpb/applicationserver_pubsub.pb.paths.fm.go @@ -74,6 +74,8 @@ var ApplicationPubSubFieldPathsNested = []string{ "updated_at", "uplink_message", "uplink_message.topic", + "uplink_normalized", + "uplink_normalized.topic", } var ApplicationPubSubFieldPathsTopLevel = []string{ @@ -95,6 +97,7 @@ var ApplicationPubSubFieldPathsTopLevel = []string{ "service_data", "updated_at", "uplink_message", + "uplink_normalized", } var ApplicationPubSubsFieldPathsNested = []string{ "pubsubs", @@ -196,6 +199,8 @@ var SetApplicationPubSubRequestFieldPathsNested = []string{ "pubsub.updated_at", "pubsub.uplink_message", "pubsub.uplink_message.topic", + "pubsub.uplink_normalized", + "pubsub.uplink_normalized.topic", } var SetApplicationPubSubRequestFieldPathsTopLevel = []string{ diff --git a/pkg/ttnpb/applicationserver_pubsub.pb.setters.fm.go b/pkg/ttnpb/applicationserver_pubsub.pb.setters.fm.go index a6b1a246b7..167a55f091 100644 --- a/pkg/ttnpb/applicationserver_pubsub.pb.setters.fm.go +++ b/pkg/ttnpb/applicationserver_pubsub.pb.setters.fm.go @@ -191,6 +191,31 @@ func (dst *ApplicationPubSub) SetFields(src *ApplicationPubSub, paths ...string) dst.UplinkMessage = nil } } + case "uplink_normalized": + if len(subs) > 0 { + var newDst, newSrc *ApplicationPubSub_Message + if (src == nil || src.UplinkNormalized == nil) && dst.UplinkNormalized == nil { + continue + } + if src != nil { + newSrc = src.UplinkNormalized + } + if dst.UplinkNormalized != nil { + newDst = dst.UplinkNormalized + } else { + newDst = &ApplicationPubSub_Message{} + dst.UplinkNormalized = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.UplinkNormalized = src.UplinkNormalized + } else { + dst.UplinkNormalized = nil + } + } case "join_accept": if len(subs) > 0 { var newDst, newSrc *ApplicationPubSub_Message diff --git a/pkg/ttnpb/applicationserver_pubsub.pb.validate.go b/pkg/ttnpb/applicationserver_pubsub.pb.validate.go index c02334058d..edcc2ab7ff 100644 --- a/pkg/ttnpb/applicationserver_pubsub.pb.validate.go +++ b/pkg/ttnpb/applicationserver_pubsub.pb.validate.go @@ -273,6 +273,18 @@ func (m *ApplicationPubSub) ValidateFields(paths ...string) error { } } + case "uplink_normalized": + + if v, ok := interface{}(m.GetUplinkNormalized()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationPubSubValidationError{ + field: "uplink_normalized", + reason: "embedded message failed validation", + cause: err, + } + } + } + case "join_accept": if v, ok := interface{}(m.GetJoinAccept()).(interface{ ValidateFields(...string) error }); ok { diff --git a/pkg/ttnpb/applicationserver_pubsub_flags.pb.go b/pkg/ttnpb/applicationserver_pubsub_flags.pb.go index e65cacfaf6..8e90b514f4 100644 --- a/pkg/ttnpb/applicationserver_pubsub_flags.pb.go +++ b/pkg/ttnpb/applicationserver_pubsub_flags.pb.go @@ -546,6 +546,8 @@ func AddSelectFlagsForApplicationPubSub(flags *pflag.FlagSet, prefix string, hid AddSelectFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("downlink-replace", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("uplink-message", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("uplink-message", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("uplink-message", prefix), hidden) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("uplink-normalized", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("uplink-normalized", prefix), true), flagsplugin.WithHidden(hidden))) + AddSelectFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("uplink-normalized", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("join-accept", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("join-accept", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("join-accept", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("downlink-ack", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("downlink-ack", prefix), true), flagsplugin.WithHidden(hidden))) @@ -638,6 +640,16 @@ func PathsFromSelectFlagsForApplicationPubSub(flags *pflag.FlagSet, prefix strin } else { paths = append(paths, selectPaths...) } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("uplink_normalized", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("uplink_normalized", prefix)) + } + if selectPaths, err := PathsFromSelectFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("uplink_normalized", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, selectPaths...) + } if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("join_accept", prefix)); err != nil { return nil, err } else if selected && val { @@ -742,6 +754,7 @@ func AddSetFlagsForApplicationPubSub(flags *pflag.FlagSet, prefix string, hidden AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("downlink-push", prefix), hidden) AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("downlink-replace", prefix), hidden) AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("uplink-message", prefix), hidden) + AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("uplink-normalized", prefix), hidden) AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("join-accept", prefix), hidden) AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("downlink-ack", prefix), hidden) AddSetFlagsForApplicationPubSub_Message(flags, flagsplugin.Prefix("downlink-nack", prefix), hidden) @@ -843,6 +856,16 @@ func (m *ApplicationPubSub) SetFromFlags(flags *pflag.FlagSet, prefix string) (p paths = append(paths, setPaths...) } } + if changed := flagsplugin.IsAnyPrefixSet(flags, flagsplugin.Prefix("uplink_normalized", prefix)); changed { + if m.UplinkNormalized == nil { + m.UplinkNormalized = &ApplicationPubSub_Message{} + } + if setPaths, err := m.UplinkNormalized.SetFromFlags(flags, flagsplugin.Prefix("uplink_normalized", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, setPaths...) + } + } if changed := flagsplugin.IsAnyPrefixSet(flags, flagsplugin.Prefix("join_accept", prefix)); changed { if m.JoinAccept == nil { m.JoinAccept = &ApplicationPubSub_Message{} diff --git a/pkg/ttnpb/applicationserver_pubsub_json.pb.go b/pkg/ttnpb/applicationserver_pubsub_json.pb.go index 4c61527e8f..01147b77d7 100644 --- a/pkg/ttnpb/applicationserver_pubsub_json.pb.go +++ b/pkg/ttnpb/applicationserver_pubsub_json.pb.go @@ -265,6 +265,12 @@ func (x *ApplicationPubSub) MarshalProtoJSON(s *jsonplugin.MarshalState) { // NOTE: ApplicationPubSub_Message does not seem to implement MarshalProtoJSON. gogo.MarshalMessage(s, x.UplinkMessage) } + if x.UplinkNormalized != nil || s.HasField("uplink_normalized") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("uplink_normalized") + // NOTE: ApplicationPubSub_Message does not seem to implement MarshalProtoJSON. + gogo.MarshalMessage(s, x.UplinkNormalized) + } if x.JoinAccept != nil || s.HasField("join_accept") { s.WriteMoreIf(&wroteField) s.WriteObjectField("join_accept") @@ -437,6 +443,16 @@ func (x *ApplicationPubSub) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { var v ApplicationPubSub_Message gogo.UnmarshalMessage(s, &v) x.UplinkMessage = &v + case "uplink_normalized", "uplinkNormalized": + s.AddField("uplink_normalized") + if s.ReadNil() { + x.UplinkNormalized = nil + return + } + // NOTE: ApplicationPubSub_Message does not seem to implement UnmarshalProtoJSON. + var v ApplicationPubSub_Message + gogo.UnmarshalMessage(s, &v) + x.UplinkNormalized = &v case "join_accept", "joinAccept": s.AddField("join_accept") if s.ReadNil() { diff --git a/pkg/ttnpb/applicationserver_web.pb.go b/pkg/ttnpb/applicationserver_web.pb.go index c7f9646acb..4cae2f9f49 100644 --- a/pkg/ttnpb/applicationserver_web.pb.go +++ b/pkg/ttnpb/applicationserver_web.pb.go @@ -213,6 +213,7 @@ type ApplicationWebhookTemplate struct { // Control the creation of the downlink queue operations API key. CreateDownlinkApiKey bool `protobuf:"varint,19,opt,name=create_downlink_api_key,json=createDownlinkApiKey,proto3" json:"create_downlink_api_key,omitempty"` UplinkMessage *ApplicationWebhookTemplate_Message `protobuf:"bytes,11,opt,name=uplink_message,json=uplinkMessage,proto3" json:"uplink_message,omitempty"` + UplinkNormalized *ApplicationWebhookTemplate_Message `protobuf:"bytes,23,opt,name=uplink_normalized,json=uplinkNormalized,proto3" json:"uplink_normalized,omitempty"` JoinAccept *ApplicationWebhookTemplate_Message `protobuf:"bytes,12,opt,name=join_accept,json=joinAccept,proto3" json:"join_accept,omitempty"` DownlinkAck *ApplicationWebhookTemplate_Message `protobuf:"bytes,13,opt,name=downlink_ack,json=downlinkAck,proto3" json:"downlink_ack,omitempty"` DownlinkNack *ApplicationWebhookTemplate_Message `protobuf:"bytes,14,opt,name=downlink_nack,json=downlinkNack,proto3" json:"downlink_nack,omitempty"` @@ -336,6 +337,13 @@ func (m *ApplicationWebhookTemplate) GetUplinkMessage() *ApplicationWebhookTempl return nil } +func (m *ApplicationWebhookTemplate) GetUplinkNormalized() *ApplicationWebhookTemplate_Message { + if m != nil { + return m.UplinkNormalized + } + return nil +} + func (m *ApplicationWebhookTemplate) GetJoinAccept() *ApplicationWebhookTemplate_Message { if m != nil { return m.JoinAccept @@ -671,6 +679,7 @@ type ApplicationWebhook struct { // The field is provided for convenience reasons, and can contain API keys with additional rights (albeit this is discouraged). DownlinkApiKey string `protobuf:"bytes,17,opt,name=downlink_api_key,json=downlinkApiKey,proto3" json:"downlink_api_key,omitempty"` UplinkMessage *ApplicationWebhook_Message `protobuf:"bytes,7,opt,name=uplink_message,json=uplinkMessage,proto3" json:"uplink_message,omitempty"` + UplinkNormalized *ApplicationWebhook_Message `protobuf:"bytes,22,opt,name=uplink_normalized,json=uplinkNormalized,proto3" json:"uplink_normalized,omitempty"` JoinAccept *ApplicationWebhook_Message `protobuf:"bytes,8,opt,name=join_accept,json=joinAccept,proto3" json:"join_accept,omitempty"` DownlinkAck *ApplicationWebhook_Message `protobuf:"bytes,9,opt,name=downlink_ack,json=downlinkAck,proto3" json:"downlink_ack,omitempty"` DownlinkNack *ApplicationWebhook_Message `protobuf:"bytes,10,opt,name=downlink_nack,json=downlinkNack,proto3" json:"downlink_nack,omitempty"` @@ -781,6 +790,13 @@ func (m *ApplicationWebhook) GetUplinkMessage() *ApplicationWebhook_Message { return nil } +func (m *ApplicationWebhook) GetUplinkNormalized() *ApplicationWebhook_Message { + if m != nil { + return m.UplinkNormalized + } + return nil +} + func (m *ApplicationWebhook) GetJoinAccept() *ApplicationWebhook_Message { if m != nil { return m.JoinAccept @@ -1253,130 +1269,132 @@ func init() { } var fileDescriptor_2652f2d8eaceda0e = []byte{ - // 1959 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x59, 0x4f, 0x73, 0x1b, 0x49, - 0x15, 0xcf, 0xc8, 0xff, 0xa4, 0x27, 0xff, 0x51, 0xda, 0x76, 0x76, 0x56, 0x76, 0xb2, 0xae, 0x49, - 0x48, 0x1c, 0x13, 0x49, 0x5b, 0x0e, 0x66, 0x89, 0x8b, 0xda, 0xac, 0xb4, 0x8e, 0xed, 0xb0, 0x98, - 0x25, 0xa3, 0x84, 0x25, 0x9b, 0x5a, 0x44, 0x5b, 0xd3, 0x96, 0x06, 0x8d, 0x66, 0x66, 0xa7, 0x5b, - 0x36, 0x22, 0x95, 0xaa, 0x2d, 0x8a, 0x03, 0x70, 0xa1, 0x8a, 0x3d, 0x50, 0xc5, 0x81, 0x82, 0x82, - 0x0b, 0x7b, 0xa4, 0xa8, 0x3d, 0xef, 0x07, 0xe0, 0xc6, 0x47, 0x80, 0x4f, 0xc0, 0x89, 0xca, 0x89, - 0x9a, 0x9e, 0x1e, 0x69, 0xfe, 0x48, 0xf1, 0x8c, 0xcc, 0xee, 0x49, 0xd3, 0xd3, 0xef, 0xfd, 0xde, - 0xeb, 0xd7, 0xaf, 0xdf, 0xfb, 0x4d, 0x0b, 0x4a, 0x86, 0xe5, 0xe0, 0x33, 0x6c, 0x96, 0x28, 0xc3, - 0xcd, 0x4e, 0x05, 0xdb, 0x7a, 0x05, 0xdb, 0xb6, 0xa1, 0x37, 0x31, 0xd3, 0x2d, 0x93, 0x12, 0xe7, - 0x94, 0x38, 0x8d, 0x33, 0x72, 0x5c, 0xb6, 0x1d, 0x8b, 0x59, 0x68, 0x91, 0x31, 0xb3, 0x2c, 0x54, - 0xca, 0xa7, 0x77, 0x8b, 0x7b, 0x2d, 0x9d, 0xb5, 0x7b, 0xc7, 0xe5, 0xa6, 0xd5, 0xad, 0x3c, 0x6e, - 0x93, 0xc7, 0x6d, 0xdd, 0x6c, 0xd1, 0x87, 0xa6, 0xd6, 0xa3, 0xcc, 0xd1, 0x09, 0xad, 0x70, 0xad, - 0x66, 0xa9, 0x45, 0xcc, 0x52, 0xcb, 0x2a, 0x9d, 0x18, 0xb8, 0x45, 0x2b, 0xd8, 0x34, 0x2d, 0xe6, - 0xc1, 0x7b, 0xa8, 0xc5, 0x6a, 0x00, 0x85, 0x98, 0xa7, 0x56, 0xdf, 0x76, 0xac, 0x9f, 0xf6, 0x83, - 0xca, 0xa7, 0xd8, 0xd0, 0x35, 0xcc, 0x48, 0x25, 0xf6, 0x20, 0x20, 0x4a, 0x01, 0x88, 0x96, 0xd5, - 0xb2, 0x3c, 0xe5, 0xe3, 0xde, 0x09, 0x1f, 0xf1, 0x01, 0x7f, 0x12, 0xe2, 0xeb, 0x2d, 0xcb, 0x6a, - 0x19, 0xc4, 0x5b, 0x6f, 0xcc, 0x9f, 0x35, 0x31, 0x3b, 0xc0, 0x20, 0x5d, 0x9b, 0xf5, 0xc5, 0xe4, - 0x46, 0x74, 0xf2, 0x44, 0x27, 0x86, 0xd6, 0xe8, 0x62, 0xda, 0x11, 0x12, 0x6f, 0x44, 0x25, 0x98, - 0xde, 0x25, 0x94, 0xe1, 0xae, 0x2d, 0x04, 0xae, 0xc6, 0x83, 0x4e, 0x1c, 0xc7, 0x72, 0xc4, 0xf4, - 0xf5, 0xf8, 0xb4, 0xae, 0x11, 0x93, 0xe9, 0x27, 0x3a, 0x71, 0x84, 0x8f, 0xca, 0x3f, 0x24, 0xb8, - 0x5a, 0x1d, 0xee, 0xd4, 0x07, 0xe4, 0xb8, 0x6d, 0x59, 0x9d, 0x87, 0x43, 0x39, 0xf4, 0x14, 0x96, - 0x02, 0x5b, 0xd9, 0xd0, 0x35, 0x2a, 0x4b, 0x1b, 0xd2, 0x66, 0x7e, 0xfb, 0x66, 0x39, 0xbc, 0x8b, - 0xe5, 0x00, 0x4e, 0x00, 0xa0, 0x96, 0x7d, 0x59, 0x9b, 0xf9, 0xb5, 0x94, 0x29, 0x48, 0xea, 0x22, - 0x0e, 0x4a, 0x50, 0xb4, 0x0f, 0x70, 0xe6, 0x19, 0x6c, 0xe8, 0x9a, 0x9c, 0xd9, 0x90, 0x36, 0x73, - 0xb5, 0x5b, 0x2f, 0x6b, 0x37, 0x1c, 0x45, 0xbe, 0xb1, 0x7d, 0xed, 0x47, 0xcf, 0x70, 0xe9, 0x67, - 0x6f, 0x96, 0xee, 0x7d, 0xb4, 0x79, 0x7f, 0xf7, 0x59, 0xe9, 0xa3, 0xfb, 0xfe, 0xf0, 0xf6, 0xf3, - 0xed, 0x3b, 0x2f, 0x6e, 0xa8, 0xb9, 0x33, 0xdf, 0xd7, 0xdd, 0xec, 0x7f, 0x3e, 0x7b, 0x7d, 0x3a, - 0x2b, 0x15, 0x24, 0xe5, 0x39, 0x7c, 0x2d, 0xbe, 0x9a, 0xc7, 0xa4, 0x6b, 0x1b, 0x98, 0x91, 0xe0, - 0xaa, 0x0e, 0x21, 0xcf, 0xc4, 0x6b, 0xd7, 0xb6, 0x94, 0xce, 0x36, 0xb0, 0x01, 0x64, 0xc0, 0xf8, - 0x2f, 0x32, 0xf0, 0xc6, 0x78, 0xeb, 0xfb, 0xee, 0xfe, 0xa2, 0xb7, 0x20, 0x93, 0xde, 0x5c, 0x46, - 0xd7, 0xd0, 0x1a, 0x4c, 0x9b, 0xb8, 0x4b, 0x44, 0x94, 0xe6, 0x5e, 0xd6, 0xa6, 0x9d, 0x8c, 0xbc, - 0xa2, 0xf2, 0x97, 0xe8, 0x36, 0xe4, 0x35, 0x42, 0x9b, 0x8e, 0x6e, 0xbb, 0x86, 0xe5, 0xa9, 0xa0, - 0x8c, 0xa6, 0x06, 0xe7, 0xd0, 0x15, 0x98, 0xa5, 0xa4, 0xe9, 0x10, 0x26, 0x4f, 0x6f, 0x48, 0x9b, - 0x59, 0x55, 0x8c, 0xd0, 0x1d, 0x58, 0xd0, 0xc8, 0x09, 0xee, 0x19, 0xac, 0x71, 0x8a, 0x8d, 0x1e, - 0x91, 0x67, 0xc2, 0x20, 0xf3, 0x62, 0xf6, 0x07, 0xee, 0x24, 0x2a, 0x42, 0xd6, 0xe2, 0x78, 0xd8, - 0x90, 0x67, 0x39, 0xce, 0x60, 0xac, 0x7c, 0xb1, 0x00, 0xc5, 0xf1, 0x61, 0x40, 0x8f, 0x60, 0x6a, - 0x98, 0x43, 0x3b, 0xaf, 0xc8, 0xa1, 0xf1, 0xbb, 0x17, 0x48, 0x29, 0x17, 0xeb, 0xff, 0x16, 0x9b, - 0xeb, 0x90, 0x35, 0xac, 0x96, 0xd5, 0xe8, 0x39, 0x06, 0x8f, 0x4e, 0x8e, 0x1b, 0x72, 0xa6, 0x7e, - 0x29, 0x49, 0xea, 0x9c, 0x3b, 0xf3, 0xc4, 0x31, 0x5c, 0x21, 0xdd, 0x3c, 0xf1, 0x84, 0x66, 0xa2, - 0x42, 0xee, 0x8c, 0x2b, 0xb4, 0x03, 0x97, 0x35, 0xab, 0xd9, 0xeb, 0x12, 0xd3, 0x2b, 0x09, 0x5c, - 0x7a, 0x36, 0x22, 0x5d, 0x08, 0x89, 0x08, 0xec, 0x63, 0x4c, 0x09, 0x97, 0x9e, 0x8b, 0x62, 0xbb, - 0x33, 0xae, 0x50, 0x1b, 0xe6, 0xda, 0x04, 0x6b, 0xc4, 0xa1, 0x72, 0x76, 0x63, 0x6a, 0x33, 0xbf, - 0xfd, 0x56, 0xf2, 0x20, 0x96, 0x0f, 0x3d, 0xcd, 0x07, 0x26, 0x73, 0xfa, 0xb5, 0xd5, 0x97, 0x35, - 0xf4, 0x7b, 0x69, 0xa9, 0xb0, 0xad, 0xb8, 0xc1, 0x78, 0x67, 0x6b, 0xc6, 0x99, 0x92, 0x3f, 0xc9, - 0xa8, 0x3e, 0x3c, 0xba, 0x0f, 0xb3, 0x27, 0x96, 0xd3, 0xc5, 0x4c, 0xce, 0x05, 0x13, 0x76, 0xe5, - 0xdc, 0x84, 0x15, 0x6a, 0xe8, 0x00, 0x66, 0x79, 0x59, 0xa3, 0x32, 0x70, 0x4f, 0x2b, 0xc9, 0x3d, - 0xe5, 0xc7, 0x45, 0x15, 0xea, 0x68, 0x07, 0x5e, 0x6b, 0x3a, 0xc4, 0x3d, 0xac, 0x9a, 0x75, 0x66, - 0x1a, 0xba, 0xd9, 0x69, 0x60, 0x5b, 0x6f, 0x74, 0x48, 0x5f, 0x5e, 0xe6, 0xe9, 0xb7, 0xe2, 0x4d, - 0xef, 0x89, 0xd9, 0xaa, 0xad, 0xbf, 0x47, 0xfa, 0xe8, 0x29, 0x2c, 0xf6, 0x6c, 0x2e, 0xdd, 0x25, - 0x94, 0xe2, 0x16, 0x91, 0xf3, 0x3c, 0xed, 0xb6, 0x53, 0x44, 0xec, 0xc8, 0xd3, 0x54, 0x17, 0x3c, - 0x24, 0x31, 0x44, 0x75, 0xc8, 0xff, 0xc4, 0xd2, 0xcd, 0x06, 0x6e, 0x36, 0x89, 0xcd, 0xe4, 0xf9, - 0x89, 0x71, 0xc1, 0x85, 0xa9, 0x72, 0x14, 0xf4, 0x04, 0xe6, 0x87, 0xeb, 0x6b, 0x76, 0xe4, 0x85, - 0x89, 0x51, 0xf3, 0x3e, 0x4e, 0xb5, 0xd9, 0x41, 0x1f, 0xc0, 0xc2, 0x00, 0xd6, 0x74, 0x71, 0x17, - 0x27, 0xc6, 0x1d, 0xf8, 0xf7, 0x3d, 0x1c, 0x01, 0xa6, 0xc4, 0x64, 0xf2, 0xd2, 0xc5, 0x81, 0xeb, - 0xc4, 0x64, 0xe8, 0x19, 0x2c, 0x0d, 0x80, 0x4f, 0xb0, 0x6e, 0x10, 0x4d, 0x2e, 0x4c, 0x0c, 0xbd, - 0xe8, 0x43, 0xed, 0x73, 0xa4, 0x10, 0xf8, 0xc7, 0x3d, 0xd2, 0x23, 0x9a, 0x7c, 0xf9, 0xe2, 0xe0, - 0x8f, 0x38, 0x12, 0xb2, 0xa1, 0x18, 0x06, 0x6f, 0xe8, 0xa6, 0x4f, 0x32, 0x34, 0x79, 0x75, 0x62, - 0x3b, 0x72, 0xc8, 0xce, 0xc3, 0x21, 0xa6, 0xbb, 0x1c, 0xc3, 0x12, 0xdd, 0x99, 0x5a, 0xc6, 0x29, - 0xd1, 0x64, 0x34, 0xf9, 0x72, 0x7c, 0xa8, 0x3a, 0x47, 0x72, 0x33, 0xd2, 0x65, 0x6f, 0x7a, 0x93, - 0x34, 0x34, 0xcc, 0xb0, 0xbc, 0x32, 0x79, 0x46, 0x0a, 0x9c, 0x3d, 0xcc, 0x30, 0xba, 0x07, 0x30, - 0xe4, 0x3b, 0xf2, 0x15, 0x0e, 0x5a, 0x2c, 0x7b, 0x84, 0xa7, 0xec, 0x13, 0x9e, 0x32, 0xaf, 0x01, - 0x47, 0x98, 0x76, 0xd4, 0xdc, 0x89, 0xff, 0x58, 0xdc, 0x85, 0xf9, 0x60, 0x11, 0x43, 0x05, 0x98, - 0x72, 0xcb, 0x00, 0x6f, 0xa9, 0xaa, 0xfb, 0x88, 0x56, 0x60, 0xc6, 0x6b, 0x61, 0xbc, 0x1f, 0xa8, - 0xde, 0x60, 0x37, 0xf3, 0x2d, 0xa9, 0x78, 0x13, 0xe6, 0xfc, 0xf3, 0xbb, 0x06, 0xd3, 0x36, 0x66, - 0x6d, 0xd1, 0x8a, 0x45, 0x3f, 0x78, 0x47, 0xe5, 0x2f, 0x95, 0x16, 0xac, 0x8d, 0x5f, 0x91, 0x4b, - 0x1e, 0x72, 0x3e, 0x01, 0x70, 0x1b, 0x99, 0x5b, 0xd9, 0xb6, 0x92, 0x47, 0x44, 0x1d, 0x2a, 0x2b, - 0x9f, 0x4d, 0x83, 0x1c, 0x97, 0x3c, 0x24, 0xd8, 0x60, 0x6d, 0xd4, 0xe0, 0x85, 0xde, 0x60, 0xed, - 0xbe, 0xe8, 0x96, 0xef, 0x9e, 0x6f, 0xc4, 0x53, 0x2d, 0x87, 0x46, 0x75, 0x86, 0x59, 0x8f, 0x7a, - 0xcf, 0xfd, 0xc3, 0x4b, 0xaa, 0x8f, 0x8a, 0x08, 0xe4, 0x7a, 0xa6, 0x6f, 0x22, 0xc3, 0x4d, 0x3c, - 0xb8, 0x88, 0x89, 0x27, 0x3e, 0xd8, 0xe1, 0x25, 0x75, 0x88, 0x5c, 0xbc, 0x09, 0xc5, 0xf1, 0xfe, - 0x0c, 0xf8, 0xd3, 0xa5, 0xe2, 0xaf, 0x32, 0xb0, 0xfe, 0x2a, 0x54, 0x74, 0x0b, 0x96, 0xbc, 0x62, - 0xd0, 0xc0, 0xcc, 0x8d, 0x21, 0xf3, 0x68, 0xc4, 0xb4, 0xba, 0xe8, 0xbd, 0xae, 0x8a, 0xb7, 0xe8, - 0x29, 0x5c, 0x31, 0x30, 0x65, 0x8d, 0xb0, 0x74, 0x03, 0x33, 0xb1, 0xca, 0x78, 0xaa, 0x3d, 0xf6, - 0xb9, 0x35, 0xef, 0xb8, 0x7f, 0x93, 0x32, 0x59, 0x49, 0x5d, 0x76, 0x31, 0xf6, 0x83, 0xc8, 0x55, - 0xb7, 0x32, 0xad, 0x8d, 0x82, 0xd6, 0x08, 0xc3, 0xba, 0x41, 0x39, 0xbd, 0xc8, 0x6f, 0xaf, 0x47, - 0xa3, 0xf8, 0xc0, 0xe5, 0xe5, 0x7b, 0x9e, 0x8c, 0x2a, 0xc7, 0x70, 0xc5, 0xcc, 0x30, 0x16, 0xc3, - 0xa7, 0x5a, 0x16, 0x66, 0x29, 0x8f, 0x83, 0xf2, 0xf9, 0x22, 0xa0, 0xf8, 0x76, 0x84, 0x09, 0x55, - 0xe9, 0xfc, 0xfd, 0x0b, 0x12, 0xa9, 0x82, 0x4f, 0xa4, 0xb8, 0xb9, 0x4b, 0x9b, 0x82, 0x50, 0xbd, - 0x0b, 0xe0, 0xf5, 0x53, 0x2d, 0x61, 0xcc, 0x3c, 0xf5, 0xc2, 0x25, 0x35, 0x27, 0xf4, 0xaa, 0xcc, - 0x05, 0xe9, 0xd9, 0x9a, 0x0f, 0x32, 0x95, 0x06, 0x44, 0xe8, 0x55, 0x59, 0x88, 0x11, 0x4d, 0x8f, - 0x63, 0x44, 0x3f, 0x1e, 0x32, 0xa2, 0x99, 0xa4, 0x3c, 0x23, 0x01, 0x13, 0xda, 0x18, 0xc5, 0x84, - 0x66, 0x27, 0x63, 0x42, 0x3f, 0x84, 0xf9, 0xc0, 0xf7, 0x06, 0x15, 0x8d, 0x72, 0x32, 0xfa, 0xab, - 0xe6, 0x87, 0x9f, 0x1f, 0x14, 0x35, 0x60, 0x69, 0x80, 0x2c, 0xc8, 0x56, 0x81, 0x07, 0xe1, 0x9b, - 0x09, 0x82, 0x10, 0x62, 0x5b, 0x5e, 0x2c, 0xd4, 0x45, 0x16, 0x7a, 0x89, 0xb6, 0xa1, 0x10, 0x23, - 0x5d, 0x97, 0x03, 0x5b, 0x21, 0x7f, 0x22, 0x0d, 0xbb, 0xa0, 0x20, 0x5e, 0x8f, 0x62, 0xc4, 0x6b, - 0x8e, 0x2f, 0x38, 0x41, 0x99, 0x1c, 0x47, 0xb8, 0xde, 0x0b, 0x13, 0xae, 0x6c, 0x6a, 0xbc, 0x20, - 0xd1, 0x3a, 0x8a, 0x10, 0xad, 0x5c, 0x6a, 0xb4, 0x10, 0xc1, 0x7a, 0x3f, 0x4a, 0xb0, 0x20, 0x35, - 0x5e, 0x98, 0x58, 0xbd, 0x1f, 0x25, 0x56, 0xf9, 0xc9, 0x01, 0x39, 0xa1, 0xaa, 0xc7, 0x09, 0xd5, - 0x7c, 0x6a, 0xc8, 0x28, 0x91, 0xaa, 0xc7, 0x89, 0xd4, 0xc2, 0xe4, 0xa0, 0x82, 0x40, 0xb5, 0x5f, - 0x49, 0xa0, 0x96, 0x53, 0xe3, 0x8f, 0x27, 0x4e, 0xf5, 0x38, 0x71, 0x5a, 0x4c, 0xef, 0x7e, 0x84, - 0x30, 0x1d, 0x45, 0x08, 0x13, 0x4a, 0x9f, 0x59, 0x41, 0xa2, 0x74, 0x04, 0x0b, 0x5e, 0xf7, 0x6b, - 0x78, 0x4d, 0x40, 0x10, 0xb0, 0xcd, 0xa4, 0x6d, 0x5a, 0x9d, 0x6f, 0x07, 0x5a, 0x69, 0x84, 0x77, - 0xad, 0x7e, 0x55, 0xbc, 0xab, 0x0a, 0xcb, 0x23, 0x2a, 0x4d, 0x2a, 0x88, 0x37, 0x93, 0x51, 0xb7, - 0xe1, 0x75, 0x4c, 0xe0, 0x62, 0xe6, 0x09, 0x2c, 0xc7, 0xe3, 0x43, 0xd1, 0xdb, 0x90, 0x15, 0x77, - 0x48, 0x3e, 0x8b, 0x53, 0xce, 0x0f, 0xab, 0x3a, 0xd0, 0x51, 0xfe, 0x2a, 0xc1, 0xeb, 0x71, 0x81, - 0x7d, 0x5e, 0xf0, 0x29, 0xfa, 0x3e, 0xcc, 0x79, 0xb5, 0xdf, 0x07, 0x4f, 0x50, 0x8f, 0x85, 0x6e, - 0x59, 0xfc, 0x7a, 0xf5, 0xd8, 0x87, 0x71, 0x77, 0x20, 0x38, 0x91, 0x26, 0x7c, 0xca, 0x5f, 0x24, - 0x58, 0x3f, 0x20, 0x6c, 0xc4, 0x7a, 0xc8, 0xc7, 0x3d, 0x42, 0x19, 0x7a, 0x78, 0x01, 0x16, 0x11, - 0xb9, 0x8e, 0x09, 0x27, 0x59, 0x26, 0x45, 0x92, 0x29, 0x9f, 0x4b, 0x70, 0xed, 0xbb, 0x3a, 0x1d, - 0xe1, 0x27, 0xf5, 0x1d, 0xfd, 0x12, 0xef, 0x23, 0x2f, 0xe0, 0xf8, 0x9f, 0x24, 0x58, 0xaf, 0xbf, - 0x2a, 0xbe, 0xfb, 0x30, 0x27, 0x12, 0x47, 0xb8, 0x9b, 0x20, 0xd7, 0x02, 0xae, 0xfa, 0xca, 0x17, - 0xf1, 0xf1, 0xef, 0x12, 0xdc, 0x18, 0x99, 0x03, 0x83, 0x2f, 0x13, 0xe1, 0xeb, 0x97, 0x70, 0x45, - 0x77, 0x01, 0xb7, 0x9b, 0x70, 0x73, 0x74, 0x4a, 0x0c, 0x3e, 0xc8, 0x7c, 0xbf, 0xc3, 0x46, 0xa4, - 0x14, 0x46, 0xb6, 0x7f, 0x93, 0x1b, 0x75, 0x69, 0xa9, 0x92, 0x96, 0x4e, 0xdd, 0xa3, 0x66, 0x00, - 0x1c, 0x10, 0xe6, 0x1f, 0xed, 0x2b, 0x31, 0xcc, 0x07, 0x5d, 0x9b, 0xf5, 0x8b, 0xb7, 0x13, 0x9f, - 0x70, 0x65, 0xed, 0xe7, 0xff, 0xfc, 0xf7, 0xa7, 0x99, 0x55, 0xb4, 0x5c, 0xc1, 0xb4, 0x22, 0xf6, - 0xb6, 0x24, 0x0e, 0x3a, 0xfa, 0xa3, 0x04, 0xf9, 0x03, 0xc2, 0x06, 0x57, 0xa6, 0xdf, 0x88, 0xe2, - 0x26, 0xd9, 0xc5, 0x62, 0x8a, 0x4f, 0x52, 0xa5, 0xc2, 0xdd, 0xb9, 0x8d, 0x6e, 0x05, 0xdd, 0x19, - 0x7c, 0xa6, 0x56, 0x9e, 0xeb, 0x1a, 0x2d, 0x07, 0xf8, 0xeb, 0x0b, 0xf4, 0xa9, 0x04, 0x0b, 0xee, - 0xae, 0x0c, 0x3f, 0x8a, 0x63, 0xe5, 0x2d, 0xd9, 0xa6, 0x15, 0xbf, 0x9e, 0xdc, 0x4d, 0xaa, 0x5c, - 0xe5, 0x7e, 0xbe, 0x86, 0x56, 0x47, 0xfa, 0x89, 0xfe, 0x2c, 0xc1, 0xd4, 0x01, 0x61, 0xe8, 0x4e, - 0xa2, 0x80, 0xf9, 0x1e, 0x24, 0x38, 0x89, 0xca, 0x77, 0xb8, 0xe1, 0x3d, 0x54, 0x0b, 0x18, 0x16, - 0x71, 0x89, 0x54, 0xa3, 0xc8, 0xf8, 0x85, 0x27, 0x34, 0xfc, 0x9f, 0xe3, 0x05, 0xfa, 0xad, 0x04, - 0xd3, 0x6e, 0x70, 0x50, 0x39, 0x59, 0xc8, 0x06, 0xa1, 0xba, 0x7e, 0xbe, 0xa3, 0x54, 0xd9, 0xe1, - 0x9e, 0x56, 0x50, 0x29, 0xec, 0xe9, 0x39, 0x5e, 0xa2, 0xff, 0x4a, 0x30, 0x55, 0x1f, 0x15, 0xba, - 0xfa, 0x45, 0x43, 0xf7, 0x07, 0x89, 0x7b, 0xf4, 0x3b, 0xa9, 0xa8, 0x86, 0x5d, 0x12, 0x4f, 0xe5, - 0x44, 0x41, 0x0c, 0x0a, 0x07, 0x82, 0xb9, 0x2b, 0x6d, 0x7d, 0xf8, 0xb6, 0x72, 0x6f, 0x62, 0xe0, - 0x5d, 0x69, 0xcb, 0xcd, 0xe5, 0xd9, 0x3d, 0x62, 0x10, 0x46, 0x50, 0xba, 0xc6, 0x57, 0x1c, 0x53, - 0x08, 0x94, 0x1a, 0x5f, 0xf1, 0xb7, 0xb7, 0x76, 0x53, 0xed, 0xc1, 0xc0, 0x71, 0x77, 0x50, 0xdb, - 0xf9, 0xe2, 0x5f, 0xd7, 0xa4, 0x0f, 0x2b, 0x2d, 0xab, 0xcc, 0xda, 0x84, 0xf1, 0x7f, 0x44, 0xcb, - 0x26, 0x61, 0x67, 0x96, 0xd3, 0xa9, 0x84, 0xff, 0xd9, 0x3b, 0xbd, 0x5b, 0xb1, 0x3b, 0xad, 0x0a, - 0x63, 0xa6, 0x7d, 0x7c, 0x3c, 0xcb, 0x5d, 0xb9, 0xfb, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x3f, - 0x21, 0x2c, 0x09, 0x92, 0x1d, 0x00, 0x00, + // 1990 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x99, 0x4d, 0x73, 0xdb, 0xc6, + 0x19, 0xc7, 0x05, 0xea, 0x8d, 0x7c, 0xa8, 0x17, 0x7a, 0xf5, 0x62, 0x84, 0x92, 0x1d, 0x0d, 0xec, + 0xda, 0xb2, 0x6a, 0x92, 0x19, 0xb9, 0x6a, 0x6a, 0x4d, 0x27, 0x0e, 0x19, 0x59, 0x92, 0x9b, 0x2a, + 0xa9, 0x41, 0xbb, 0xae, 0xe3, 0x49, 0xd9, 0x15, 0xb1, 0x22, 0x51, 0x82, 0x00, 0x82, 0x5d, 0x4a, + 0x55, 0x3c, 0x9e, 0xc9, 0x74, 0x7a, 0x68, 0x7b, 0xc9, 0x4c, 0x73, 0xe8, 0x4c, 0x0f, 0x9d, 0x66, + 0xda, 0x4b, 0x73, 0xec, 0x74, 0x7a, 0xee, 0x07, 0xe8, 0xad, 0x1f, 0xa1, 0xfd, 0x04, 0x3d, 0x75, + 0x7c, 0xea, 0x60, 0xb1, 0x20, 0xf1, 0x42, 0x59, 0x00, 0x55, 0xe7, 0x44, 0x2c, 0xf6, 0xd9, 0xdf, + 0xf3, 0xec, 0xb3, 0x8b, 0x7d, 0xfe, 0x00, 0xa1, 0x64, 0x58, 0x0e, 0x3e, 0xc1, 0x66, 0x89, 0x32, + 0xdc, 0xec, 0x54, 0xb0, 0xad, 0x57, 0xb0, 0x6d, 0x1b, 0x7a, 0x13, 0x33, 0xdd, 0x32, 0x29, 0x71, + 0x8e, 0x89, 0xd3, 0x38, 0x21, 0x87, 0x65, 0xdb, 0xb1, 0x98, 0x85, 0xe6, 0x18, 0x33, 0xcb, 0x62, + 0x48, 0xf9, 0xf8, 0x4e, 0x71, 0xa7, 0xa5, 0xb3, 0x76, 0xef, 0xb0, 0xdc, 0xb4, 0xba, 0x95, 0x47, + 0x6d, 0xf2, 0xa8, 0xad, 0x9b, 0x2d, 0xfa, 0xc0, 0xd4, 0x7a, 0x94, 0x39, 0x3a, 0xa1, 0x15, 0x3e, + 0xaa, 0x59, 0x6a, 0x11, 0xb3, 0xd4, 0xb2, 0x4a, 0x47, 0x06, 0x6e, 0xd1, 0x0a, 0x36, 0x4d, 0x8b, + 0x79, 0x78, 0x8f, 0x5a, 0xac, 0x06, 0x28, 0xc4, 0x3c, 0xb6, 0x4e, 0x6d, 0xc7, 0xfa, 0xd9, 0x69, + 0x70, 0xf0, 0x31, 0x36, 0x74, 0x0d, 0x33, 0x52, 0x89, 0x5d, 0x08, 0x44, 0x29, 0x80, 0x68, 0x59, + 0x2d, 0xcb, 0x1b, 0x7c, 0xd8, 0x3b, 0xe2, 0x2d, 0xde, 0xe0, 0x57, 0xc2, 0x7c, 0xb5, 0x65, 0x59, + 0x2d, 0x83, 0x78, 0xf3, 0x8d, 0xc5, 0xb3, 0x22, 0x7a, 0xfb, 0x0c, 0xd2, 0xb5, 0xd9, 0xa9, 0xe8, + 0x5c, 0x8b, 0x76, 0x1e, 0xe9, 0xc4, 0xd0, 0x1a, 0x5d, 0x4c, 0x3b, 0xc2, 0xe2, 0xcd, 0xa8, 0x05, + 0xd3, 0xbb, 0x84, 0x32, 0xdc, 0xb5, 0x85, 0xc1, 0x95, 0x78, 0xd2, 0x89, 0xe3, 0x58, 0x8e, 0xe8, + 0xbe, 0x16, 0xef, 0xd6, 0x35, 0x62, 0x32, 0xfd, 0x48, 0x27, 0x8e, 0x88, 0x51, 0xf9, 0x87, 0x04, + 0x57, 0xaa, 0x83, 0x95, 0x7a, 0x42, 0x0e, 0xdb, 0x96, 0xd5, 0x79, 0x30, 0xb0, 0x43, 0x4f, 0x61, + 0x3e, 0xb0, 0x94, 0x0d, 0x5d, 0xa3, 0xb2, 0xb4, 0x26, 0xad, 0xe7, 0x37, 0x6f, 0x94, 0xc3, 0xab, + 0x58, 0x0e, 0x70, 0x02, 0x80, 0x5a, 0xf6, 0x65, 0x6d, 0xf2, 0xd7, 0x52, 0xa6, 0x20, 0xa9, 0x73, + 0x38, 0x68, 0x41, 0xd1, 0x2e, 0xc0, 0x89, 0xe7, 0xb0, 0xa1, 0x6b, 0x72, 0x66, 0x4d, 0x5a, 0xcf, + 0xd5, 0x6e, 0xbe, 0xac, 0x5d, 0x77, 0x14, 0xf9, 0xfa, 0xe6, 0xd5, 0x1f, 0x3f, 0xc3, 0xa5, 0x4f, + 0xdf, 0x2a, 0xdd, 0xfd, 0x78, 0xfd, 0xde, 0xf6, 0xb3, 0xd2, 0xc7, 0xf7, 0xfc, 0xe6, 0xad, 0xe7, + 0x9b, 0xb7, 0x5f, 0x5c, 0x57, 0x73, 0x27, 0x7e, 0xac, 0xdb, 0xd9, 0xff, 0x7c, 0xf5, 0xc6, 0x44, + 0x56, 0x2a, 0x48, 0xca, 0x73, 0xf8, 0x46, 0x7c, 0x36, 0x8f, 0x48, 0xd7, 0x36, 0x30, 0x23, 0xc1, + 0x59, 0xed, 0x43, 0x9e, 0x89, 0xdb, 0xae, 0x6f, 0x29, 0x9d, 0x6f, 0x60, 0x7d, 0x64, 0xc0, 0xf9, + 0x2f, 0x32, 0xf0, 0xe6, 0xd9, 0xde, 0x77, 0xdd, 0xf5, 0x45, 0x6f, 0x43, 0x26, 0xbd, 0xbb, 0x8c, + 0xae, 0xa1, 0x15, 0x98, 0x30, 0x71, 0x97, 0x88, 0x2c, 0x4d, 0xbf, 0xac, 0x4d, 0x38, 0x19, 0x79, + 0x51, 0xe5, 0x37, 0xd1, 0x2d, 0xc8, 0x6b, 0x84, 0x36, 0x1d, 0xdd, 0x76, 0x1d, 0xcb, 0xe3, 0x41, + 0x1b, 0x4d, 0x0d, 0xf6, 0xa1, 0x65, 0x98, 0xa2, 0xa4, 0xe9, 0x10, 0x26, 0x4f, 0xac, 0x49, 0xeb, + 0x59, 0x55, 0xb4, 0xd0, 0x6d, 0x98, 0xd5, 0xc8, 0x11, 0xee, 0x19, 0xac, 0x71, 0x8c, 0x8d, 0x1e, + 0x91, 0x27, 0xc3, 0x90, 0x19, 0xd1, 0xfb, 0x43, 0xb7, 0x13, 0x15, 0x21, 0x6b, 0x71, 0x1e, 0x36, + 0xe4, 0x29, 0xce, 0xe9, 0xb7, 0x95, 0x2f, 0xe7, 0xa0, 0x78, 0x76, 0x1a, 0xd0, 0x43, 0x18, 0x1f, + 0xec, 0xa1, 0xad, 0x57, 0xec, 0xa1, 0xb3, 0x57, 0x2f, 0xb0, 0xa5, 0x5c, 0xd6, 0xff, 0x2d, 0x37, + 0xd7, 0x20, 0x6b, 0x58, 0x2d, 0xab, 0xd1, 0x73, 0x0c, 0x9e, 0x9d, 0x1c, 0x77, 0xe4, 0x8c, 0xff, + 0x52, 0x92, 0xd4, 0x69, 0xb7, 0xe7, 0xb1, 0x63, 0xb8, 0x46, 0xba, 0x79, 0xe4, 0x19, 0x4d, 0x46, + 0x8d, 0xdc, 0x1e, 0xd7, 0x68, 0x0b, 0x2e, 0x69, 0x56, 0xb3, 0xd7, 0x25, 0xa6, 0x77, 0x24, 0x70, + 0xeb, 0xa9, 0x88, 0x75, 0x21, 0x64, 0x22, 0xd8, 0x87, 0x98, 0x12, 0x6e, 0x3d, 0x1d, 0x65, 0xbb, + 0x3d, 0xae, 0x51, 0x1b, 0xa6, 0xdb, 0x04, 0x6b, 0xc4, 0xa1, 0x72, 0x76, 0x6d, 0x7c, 0x3d, 0xbf, + 0xf9, 0x76, 0xf2, 0x24, 0x96, 0xf7, 0xbd, 0x91, 0xf7, 0x4d, 0xe6, 0x9c, 0xd6, 0x96, 0x5e, 0xd6, + 0xd0, 0xef, 0xa4, 0xf9, 0xc2, 0xa6, 0xe2, 0x26, 0xe3, 0xdd, 0x8d, 0x49, 0x67, 0x5c, 0xfe, 0x2c, + 0xa3, 0xfa, 0x78, 0x74, 0x0f, 0xa6, 0x8e, 0x2c, 0xa7, 0x8b, 0x99, 0x9c, 0x0b, 0x6e, 0xd8, 0xc5, + 0x73, 0x37, 0xac, 0x18, 0x86, 0xf6, 0x60, 0x8a, 0x1f, 0x6b, 0x54, 0x06, 0x1e, 0x69, 0x25, 0x79, + 0xa4, 0xfc, 0x71, 0x51, 0xc5, 0x70, 0xb4, 0x05, 0x97, 0x9b, 0x0e, 0x71, 0x1f, 0x56, 0xcd, 0x3a, + 0x31, 0x0d, 0xdd, 0xec, 0x34, 0xb0, 0xad, 0x37, 0x3a, 0xe4, 0x54, 0x5e, 0xe0, 0xdb, 0x6f, 0xd1, + 0xeb, 0xde, 0x11, 0xbd, 0x55, 0x5b, 0x7f, 0x9f, 0x9c, 0xa2, 0xa7, 0x30, 0xd7, 0xb3, 0xb9, 0x75, + 0x97, 0x50, 0x8a, 0x5b, 0x44, 0xce, 0xf3, 0x6d, 0xb7, 0x99, 0x22, 0x63, 0x07, 0xde, 0x48, 0x75, + 0xd6, 0x23, 0x89, 0x26, 0x6a, 0xc0, 0x25, 0x81, 0x36, 0xdd, 0xb9, 0x1a, 0xfa, 0xa7, 0x44, 0x93, + 0x2f, 0x8f, 0x4c, 0x2f, 0x78, 0xb0, 0x0f, 0xfa, 0x2c, 0x54, 0x87, 0xfc, 0x4f, 0x2d, 0xdd, 0x6c, + 0xe0, 0x66, 0x93, 0xd8, 0x4c, 0x9e, 0x19, 0x19, 0x0d, 0x2e, 0xa6, 0xca, 0x29, 0xe8, 0x31, 0xcc, + 0x0c, 0x12, 0xd8, 0xec, 0xc8, 0xb3, 0x23, 0x53, 0xf3, 0x3e, 0xa7, 0xda, 0xec, 0xa0, 0x27, 0x30, + 0xdb, 0xc7, 0x9a, 0x2e, 0x77, 0x6e, 0x64, 0x6e, 0x3f, 0xbe, 0x0f, 0x70, 0x04, 0x4c, 0x89, 0xc9, + 0xe4, 0xf9, 0x8b, 0x83, 0xeb, 0xc4, 0x64, 0xe8, 0x19, 0xcc, 0xf7, 0xc1, 0x47, 0x58, 0x37, 0x88, + 0x26, 0x17, 0x46, 0x46, 0xcf, 0xf9, 0xa8, 0x5d, 0x4e, 0x0a, 0xc1, 0x3f, 0xe9, 0x91, 0x1e, 0xd1, + 0xe4, 0x4b, 0x17, 0x87, 0x3f, 0xe4, 0x24, 0x64, 0x43, 0x31, 0x0c, 0x6f, 0xe8, 0xa6, 0xaf, 0x62, + 0x34, 0x79, 0x69, 0x64, 0x3f, 0x72, 0xc8, 0xcf, 0x83, 0x01, 0xd3, 0x9d, 0x8e, 0x61, 0x89, 0xf2, + 0x4f, 0x2d, 0xe3, 0x98, 0x68, 0x32, 0x1a, 0x7d, 0x3a, 0x3e, 0xaa, 0xce, 0x49, 0xee, 0x8e, 0x74, + 0xe5, 0xa1, 0xde, 0x24, 0x0d, 0x0d, 0x33, 0x2c, 0x2f, 0x8e, 0xbe, 0x23, 0x05, 0x67, 0x07, 0x33, + 0x8c, 0xee, 0x02, 0x0c, 0x04, 0x95, 0xbc, 0xcc, 0xa1, 0xc5, 0xb2, 0xa7, 0xa8, 0xca, 0xbe, 0xa2, + 0x2a, 0xf3, 0x43, 0xe6, 0x00, 0xd3, 0x8e, 0x9a, 0x3b, 0xf2, 0x2f, 0x8b, 0xdb, 0x30, 0x13, 0x3c, + 0x25, 0x51, 0x01, 0xc6, 0xdd, 0x73, 0x86, 0xd7, 0x6c, 0xd5, 0xbd, 0x44, 0x8b, 0x30, 0xe9, 0xd5, + 0x48, 0x5e, 0x70, 0x54, 0xaf, 0xb1, 0x9d, 0xf9, 0x8e, 0x54, 0xbc, 0x01, 0xd3, 0xfe, 0x01, 0xb1, + 0x02, 0x13, 0x36, 0x66, 0x6d, 0x51, 0xeb, 0x45, 0xc1, 0x79, 0x57, 0xe5, 0x37, 0x95, 0x16, 0xac, + 0x9c, 0x3d, 0x23, 0x57, 0x9d, 0xe4, 0x7c, 0x85, 0xe1, 0x56, 0x4a, 0xf7, 0xe8, 0xdc, 0x48, 0x9e, + 0x11, 0x75, 0x30, 0x58, 0xf9, 0x6a, 0x02, 0xe4, 0xb8, 0xe5, 0x3e, 0xc1, 0x06, 0x6b, 0xa3, 0x06, + 0xaf, 0x24, 0x06, 0x6b, 0x9f, 0x8a, 0x72, 0xfc, 0xde, 0xf9, 0x4e, 0xbc, 0xa1, 0xe5, 0x50, 0xab, + 0xce, 0x30, 0xeb, 0x51, 0xef, 0xfa, 0x74, 0x7f, 0x4c, 0xf5, 0xa9, 0x88, 0x40, 0xae, 0x67, 0xfa, + 0x2e, 0x32, 0xdc, 0xc5, 0xfd, 0x8b, 0xb8, 0x78, 0xec, 0xc3, 0xf6, 0xc7, 0xd4, 0x01, 0xb9, 0x78, + 0x03, 0x8a, 0x67, 0xc7, 0xd3, 0x17, 0x68, 0x63, 0xc5, 0x5f, 0x65, 0x60, 0xf5, 0x55, 0x54, 0x74, + 0x13, 0xe6, 0xbd, 0xc3, 0xa0, 0x81, 0x99, 0x9b, 0x43, 0xe6, 0xe9, 0x94, 0x09, 0x75, 0xce, 0xbb, + 0x5d, 0x15, 0x77, 0xd1, 0x53, 0x58, 0x36, 0x30, 0x65, 0x8d, 0xb0, 0x75, 0x03, 0x33, 0x31, 0xcb, + 0xf8, 0x56, 0x7b, 0xe4, 0x8b, 0x77, 0x5e, 0xd2, 0xff, 0x22, 0x65, 0xb2, 0x92, 0xba, 0xe0, 0x32, + 0x76, 0x83, 0xe4, 0xaa, 0x7b, 0x32, 0xad, 0x0c, 0x43, 0x6b, 0x84, 0x61, 0xdd, 0xa0, 0x5c, 0xbf, + 0xe4, 0x37, 0x57, 0xa3, 0x59, 0xbc, 0xef, 0x0a, 0xff, 0x1d, 0xcf, 0x46, 0x95, 0x63, 0x5c, 0xd1, + 0x33, 0xc8, 0xc5, 0xe0, 0xaa, 0x96, 0x85, 0x29, 0xca, 0xf3, 0xa0, 0x7c, 0x3e, 0x0f, 0x28, 0xbe, + 0x1c, 0x61, 0xc5, 0x56, 0x3a, 0x7f, 0xfd, 0x82, 0x4a, 0xad, 0xe0, 0x2b, 0x35, 0xee, 0x6e, 0x6c, + 0x5d, 0x28, 0xb6, 0xf7, 0x00, 0xbc, 0x82, 0xad, 0x25, 0xcc, 0x99, 0x37, 0xbc, 0x30, 0xa6, 0xe6, + 0xc4, 0xb8, 0x2a, 0x73, 0x21, 0x3d, 0x5b, 0xf3, 0x21, 0xe3, 0x69, 0x20, 0x62, 0x5c, 0x95, 0x85, + 0x24, 0xd7, 0xc4, 0x59, 0x92, 0xeb, 0x27, 0x03, 0xc9, 0x35, 0x99, 0x54, 0xc8, 0x24, 0x90, 0x5a, + 0x6b, 0xc3, 0xa4, 0xd6, 0xd4, 0x68, 0x52, 0xeb, 0x47, 0x30, 0x13, 0x78, 0xa1, 0xa1, 0xa2, 0x50, + 0x8e, 0xa6, 0xaf, 0xd5, 0xfc, 0xe0, 0xfd, 0x86, 0xa2, 0x06, 0xcc, 0xf7, 0xc9, 0x42, 0xcd, 0x15, + 0x78, 0x12, 0xbe, 0x9d, 0x20, 0x09, 0x21, 0x39, 0xe7, 0xe5, 0x42, 0x9d, 0x63, 0xa1, 0x9b, 0x68, + 0x13, 0x0a, 0x31, 0x55, 0x77, 0x29, 0xb0, 0x14, 0xf2, 0x67, 0xd2, 0xa0, 0x0a, 0x0a, 0x65, 0xf7, + 0x30, 0xa6, 0xec, 0xa6, 0xf9, 0x84, 0x13, 0x1c, 0x93, 0x67, 0x29, 0xba, 0x27, 0xc3, 0x14, 0xdd, + 0x72, 0x6a, 0x6a, 0x5c, 0xc9, 0xbd, 0x1f, 0x56, 0x72, 0xd9, 0xd4, 0xc8, 0xa0, 0x82, 0x3b, 0x88, + 0x28, 0xb8, 0x5c, 0x6a, 0x5a, 0x48, 0xb9, 0x7d, 0x18, 0x55, 0x6e, 0x90, 0x9a, 0x17, 0x56, 0x6c, + 0x1f, 0x46, 0x15, 0x5b, 0x7e, 0x74, 0x20, 0x57, 0x6a, 0xf5, 0xb8, 0x52, 0x9b, 0x49, 0x8d, 0x8c, + 0x2a, 0xb4, 0x7a, 0x5c, 0xa1, 0xcd, 0x8e, 0x0e, 0x15, 0xca, 0xac, 0xfd, 0x4a, 0x65, 0xb6, 0x90, + 0x9a, 0x7f, 0xb6, 0x22, 0xab, 0xc7, 0x15, 0xd9, 0x5c, 0xfa, 0xf0, 0x23, 0x4a, 0xec, 0x20, 0xa2, + 0xc4, 0x50, 0xfa, 0x9d, 0x15, 0x54, 0x60, 0x07, 0x30, 0xeb, 0x95, 0xd5, 0x86, 0x57, 0x5d, 0x84, + 0xb2, 0x5b, 0x4f, 0x5a, 0xff, 0xd5, 0x99, 0x76, 0xa0, 0x46, 0x47, 0x04, 0xdd, 0xd2, 0xd7, 0x25, + 0xe8, 0xaa, 0xb0, 0x30, 0xe4, 0x08, 0x4b, 0x85, 0x78, 0x2b, 0x99, 0x26, 0x1c, 0x7c, 0x48, 0x0a, + 0x7c, 0x52, 0x7a, 0x0c, 0x0b, 0xf1, 0xfc, 0x50, 0xf4, 0x0e, 0x64, 0xc5, 0xd7, 0x2f, 0x5f, 0x1e, + 0x2a, 0xe7, 0xa7, 0x55, 0xed, 0x8f, 0x51, 0xfe, 0x2c, 0xc1, 0x1b, 0x71, 0x83, 0x5d, 0x5e, 0x49, + 0x28, 0xfa, 0x01, 0x4c, 0x7b, 0x45, 0xc5, 0x87, 0x27, 0x38, 0xe8, 0xc5, 0xd8, 0xb2, 0xf8, 0xf5, + 0x0e, 0x7a, 0x1f, 0xe3, 0xae, 0x40, 0xb0, 0x23, 0x4d, 0xfa, 0x94, 0x3f, 0x49, 0xb0, 0xba, 0x47, + 0xd8, 0x90, 0xf9, 0x90, 0x4f, 0x7a, 0x84, 0x32, 0xf4, 0xe0, 0x02, 0xf2, 0x24, 0xf2, 0x21, 0x29, + 0xbc, 0xc9, 0x32, 0x29, 0x36, 0x99, 0xf2, 0x37, 0x09, 0xae, 0x7e, 0x5f, 0xa7, 0x43, 0xe2, 0xa4, + 0x7e, 0xa0, 0xaf, 0xf1, 0x4b, 0xea, 0x05, 0x02, 0xff, 0x52, 0x82, 0xd5, 0xfa, 0xab, 0xf2, 0xbb, + 0x0b, 0xd3, 0x62, 0xe3, 0x88, 0x70, 0x13, 0xec, 0xb5, 0x40, 0xa8, 0xfe, 0xe0, 0x8b, 0xc4, 0xf8, + 0x57, 0x09, 0xae, 0x0f, 0xdd, 0x03, 0xfd, 0x57, 0x1e, 0x11, 0xeb, 0x6b, 0xf8, 0xb8, 0x78, 0x81, + 0xb0, 0x9b, 0x70, 0x63, 0xf8, 0x96, 0xe8, 0xbf, 0xe9, 0xf9, 0x71, 0x87, 0x9d, 0x48, 0x29, 0x9c, + 0x6c, 0x7e, 0x9e, 0x1b, 0xf6, 0xb9, 0x55, 0x25, 0x2d, 0x9d, 0xba, 0x8f, 0x9a, 0x01, 0xb0, 0x47, + 0x98, 0xff, 0x68, 0x2f, 0xc7, 0x98, 0xf7, 0xbb, 0x36, 0x3b, 0x2d, 0xde, 0x4a, 0xfc, 0x84, 0x2b, + 0x2b, 0x3f, 0xff, 0xe7, 0xbf, 0xbf, 0xc8, 0x2c, 0xa1, 0x85, 0x0a, 0xa6, 0x15, 0xb1, 0xb6, 0x25, + 0xf1, 0xa0, 0xa3, 0x3f, 0x48, 0x90, 0xdf, 0x23, 0xac, 0xff, 0xb1, 0xf7, 0x5b, 0x51, 0x6e, 0x92, + 0x55, 0x2c, 0xa6, 0x78, 0xd7, 0x55, 0x2a, 0x3c, 0x9c, 0x5b, 0xe8, 0x66, 0x30, 0x9c, 0xfe, 0xfb, + 0x6f, 0xe5, 0xb9, 0xae, 0xd1, 0x72, 0x40, 0x18, 0xbf, 0x40, 0x5f, 0x48, 0x30, 0xeb, 0xae, 0xca, + 0xe0, 0x6d, 0x3b, 0x76, 0xbc, 0x25, 0x5b, 0xb4, 0xe2, 0x37, 0x93, 0x87, 0x49, 0x95, 0x2b, 0x3c, + 0xce, 0xcb, 0x68, 0x69, 0x68, 0x9c, 0xe8, 0x8f, 0x12, 0x8c, 0xef, 0x11, 0x86, 0x6e, 0x27, 0x4a, + 0x98, 0x1f, 0x41, 0x82, 0x27, 0x51, 0xf9, 0x1e, 0x77, 0xbc, 0x83, 0x6a, 0x01, 0xc7, 0x22, 0x2f, + 0x91, 0xd3, 0x28, 0xd2, 0x7e, 0xe1, 0x19, 0x0d, 0xfe, 0xa1, 0x79, 0x81, 0x7e, 0x23, 0xc1, 0x84, + 0x9b, 0x1c, 0x54, 0x4e, 0x96, 0xb2, 0x7e, 0xaa, 0xae, 0x9d, 0x1f, 0x28, 0x55, 0xb6, 0x78, 0xa4, + 0x15, 0x54, 0x0a, 0x47, 0x7a, 0x4e, 0x94, 0xe8, 0xbf, 0x12, 0x8c, 0xd7, 0x87, 0xa5, 0xae, 0x7e, + 0xd1, 0xd4, 0xfd, 0x5e, 0xe2, 0x11, 0xfd, 0x56, 0x2a, 0xaa, 0xe1, 0x90, 0xc4, 0x55, 0x39, 0x51, + 0x12, 0x83, 0xc6, 0x81, 0x64, 0x6e, 0x4b, 0x1b, 0x1f, 0xbd, 0xa3, 0xdc, 0x1d, 0x19, 0xbc, 0x2d, + 0x6d, 0xb8, 0x7b, 0x79, 0x6a, 0x87, 0x18, 0x84, 0x11, 0x94, 0xae, 0xf0, 0x15, 0xcf, 0x38, 0x08, + 0x94, 0x1a, 0x9f, 0xf1, 0x77, 0x37, 0xb6, 0x53, 0xad, 0x41, 0x3f, 0x70, 0xb7, 0x51, 0xdb, 0xfa, + 0xfb, 0xbf, 0xae, 0x4a, 0x1f, 0x55, 0x5a, 0x56, 0x99, 0xb5, 0x09, 0xe3, 0xff, 0xe5, 0x96, 0x4d, + 0xc2, 0x4e, 0x2c, 0xa7, 0x53, 0x09, 0xff, 0x27, 0x79, 0x7c, 0xa7, 0x62, 0x77, 0x5a, 0x15, 0xc6, + 0x4c, 0xfb, 0xf0, 0x70, 0x8a, 0x87, 0x72, 0xe7, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xe0, 0x13, + 0xd7, 0xe3, 0x4c, 0x1e, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/pkg/ttnpb/applicationserver_web.pb.paths.fm.go b/pkg/ttnpb/applicationserver_web.pb.paths.fm.go index 664f1f628d..0f59d502de 100644 --- a/pkg/ttnpb/applicationserver_web.pb.paths.fm.go +++ b/pkg/ttnpb/applicationserver_web.pb.paths.fm.go @@ -70,6 +70,8 @@ var ApplicationWebhookTemplateFieldPathsNested = []string{ "service_data.path", "uplink_message", "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path", } var ApplicationWebhookTemplateFieldPathsTopLevel = []string{ @@ -95,6 +97,7 @@ var ApplicationWebhookTemplateFieldPathsTopLevel = []string{ "name", "service_data", "uplink_message", + "uplink_normalized", } var ApplicationWebhookTemplatesFieldPathsNested = []string{ "templates", @@ -183,6 +186,8 @@ var ApplicationWebhookFieldPathsNested = []string{ "updated_at", "uplink_message", "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path", } var ApplicationWebhookFieldPathsTopLevel = []string{ @@ -207,6 +212,7 @@ var ApplicationWebhookFieldPathsTopLevel = []string{ "template_ids", "updated_at", "uplink_message", + "uplink_normalized", } var ApplicationWebhooksFieldPathsNested = []string{ "webhooks", @@ -301,6 +307,8 @@ var SetApplicationWebhookRequestFieldPathsNested = []string{ "webhook.updated_at", "webhook.uplink_message", "webhook.uplink_message.path", + "webhook.uplink_normalized", + "webhook.uplink_normalized.path", } var SetApplicationWebhookRequestFieldPathsTopLevel = []string{ diff --git a/pkg/ttnpb/applicationserver_web.pb.setters.fm.go b/pkg/ttnpb/applicationserver_web.pb.setters.fm.go index 6edb2364f0..399a3a8c6f 100644 --- a/pkg/ttnpb/applicationserver_web.pb.setters.fm.go +++ b/pkg/ttnpb/applicationserver_web.pb.setters.fm.go @@ -293,6 +293,31 @@ func (dst *ApplicationWebhookTemplate) SetFields(src *ApplicationWebhookTemplate dst.UplinkMessage = nil } } + case "uplink_normalized": + if len(subs) > 0 { + var newDst, newSrc *ApplicationWebhookTemplate_Message + if (src == nil || src.UplinkNormalized == nil) && dst.UplinkNormalized == nil { + continue + } + if src != nil { + newSrc = src.UplinkNormalized + } + if dst.UplinkNormalized != nil { + newDst = dst.UplinkNormalized + } else { + newDst = &ApplicationWebhookTemplate_Message{} + dst.UplinkNormalized = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.UplinkNormalized = src.UplinkNormalized + } else { + dst.UplinkNormalized = nil + } + } case "join_accept": if len(subs) > 0 { var newDst, newSrc *ApplicationWebhookTemplate_Message @@ -803,6 +828,31 @@ func (dst *ApplicationWebhook) SetFields(src *ApplicationWebhook, paths ...strin dst.UplinkMessage = nil } } + case "uplink_normalized": + if len(subs) > 0 { + var newDst, newSrc *ApplicationWebhook_Message + if (src == nil || src.UplinkNormalized == nil) && dst.UplinkNormalized == nil { + continue + } + if src != nil { + newSrc = src.UplinkNormalized + } + if dst.UplinkNormalized != nil { + newDst = dst.UplinkNormalized + } else { + newDst = &ApplicationWebhook_Message{} + dst.UplinkNormalized = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.UplinkNormalized = src.UplinkNormalized + } else { + dst.UplinkNormalized = nil + } + } case "join_accept": if len(subs) > 0 { var newDst, newSrc *ApplicationWebhook_Message diff --git a/pkg/ttnpb/applicationserver_web.pb.validate.go b/pkg/ttnpb/applicationserver_web.pb.validate.go index e8ba19cec0..6d19cb93ce 100644 --- a/pkg/ttnpb/applicationserver_web.pb.validate.go +++ b/pkg/ttnpb/applicationserver_web.pb.validate.go @@ -572,6 +572,18 @@ func (m *ApplicationWebhookTemplate) ValidateFields(paths ...string) error { } } + case "uplink_normalized": + + if v, ok := interface{}(m.GetUplinkNormalized()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationWebhookTemplateValidationError{ + field: "uplink_normalized", + reason: "embedded message failed validation", + cause: err, + } + } + } + case "join_accept": if v, ok := interface{}(m.GetJoinAccept()).(interface{ ValidateFields(...string) error }); ok { @@ -1137,6 +1149,18 @@ func (m *ApplicationWebhook) ValidateFields(paths ...string) error { } } + case "uplink_normalized": + + if v, ok := interface{}(m.GetUplinkNormalized()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationWebhookValidationError{ + field: "uplink_normalized", + reason: "embedded message failed validation", + cause: err, + } + } + } + case "join_accept": if v, ok := interface{}(m.GetJoinAccept()).(interface{ ValidateFields(...string) error }); ok { diff --git a/pkg/ttnpb/applicationserver_web_flags.pb.go b/pkg/ttnpb/applicationserver_web_flags.pb.go index 562f409a3b..84936fa8d3 100644 --- a/pkg/ttnpb/applicationserver_web_flags.pb.go +++ b/pkg/ttnpb/applicationserver_web_flags.pb.go @@ -210,6 +210,8 @@ func AddSelectFlagsForApplicationWebhook(flags *pflag.FlagSet, prefix string, hi flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("downlink-api-key", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("downlink-api-key", prefix), false), flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("uplink-message", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("uplink-message", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("uplink-message", prefix), hidden) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("uplink-normalized", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("uplink-normalized", prefix), true), flagsplugin.WithHidden(hidden))) + AddSelectFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("uplink-normalized", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("join-accept", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("join-accept", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("join-accept", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("downlink-ack", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("downlink-ack", prefix), true), flagsplugin.WithHidden(hidden))) @@ -280,6 +282,16 @@ func PathsFromSelectFlagsForApplicationWebhook(flags *pflag.FlagSet, prefix stri } else { paths = append(paths, selectPaths...) } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("uplink_normalized", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("uplink_normalized", prefix)) + } + if selectPaths, err := PathsFromSelectFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("uplink_normalized", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, selectPaths...) + } if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("join_accept", prefix)); err != nil { return nil, err } else if selected && val { @@ -398,6 +410,7 @@ func AddSetFlagsForApplicationWebhook(flags *pflag.FlagSet, prefix string, hidde flags.AddFlag(flagsplugin.NewStringStringMapFlag(flagsplugin.Prefix("template-fields", prefix), "", flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewStringFlag(flagsplugin.Prefix("downlink-api-key", prefix), "", flagsplugin.WithHidden(hidden))) AddSetFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("uplink-message", prefix), hidden) + AddSetFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("uplink-normalized", prefix), hidden) AddSetFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("join-accept", prefix), hidden) AddSetFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("downlink-ack", prefix), hidden) AddSetFlagsForApplicationWebhook_Message(flags, flagsplugin.Prefix("downlink-nack", prefix), hidden) @@ -473,6 +486,16 @@ func (m *ApplicationWebhook) SetFromFlags(flags *pflag.FlagSet, prefix string) ( paths = append(paths, setPaths...) } } + if changed := flagsplugin.IsAnyPrefixSet(flags, flagsplugin.Prefix("uplink_normalized", prefix)); changed { + if m.UplinkNormalized == nil { + m.UplinkNormalized = &ApplicationWebhook_Message{} + } + if setPaths, err := m.UplinkNormalized.SetFromFlags(flags, flagsplugin.Prefix("uplink_normalized", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, setPaths...) + } + } if changed := flagsplugin.IsAnyPrefixSet(flags, flagsplugin.Prefix("join_accept", prefix)); changed { if m.JoinAccept == nil { m.JoinAccept = &ApplicationWebhook_Message{} diff --git a/pkg/ttnpb/messages.pb.go b/pkg/ttnpb/messages.pb.go index 0c91986f7f..56fa00904f 100644 --- a/pkg/ttnpb/messages.pb.go +++ b/pkg/ttnpb/messages.pb.go @@ -485,8 +485,10 @@ func (m *GatewayUplinkMessage) GetBandId() string { type ApplicationUplink struct { // Join Server issued identifier for the session keys used by this uplink. SessionKeyId []byte `protobuf:"bytes,1,opt,name=session_key_id,json=sessionKeyId,proto3" json:"session_key_id,omitempty"` - FPort uint32 `protobuf:"varint,2,opt,name=f_port,json=fPort,proto3" json:"f_port,omitempty"` - FCnt uint32 `protobuf:"varint,3,opt,name=f_cnt,json=fCnt,proto3" json:"f_cnt,omitempty"` + // LoRaWAN FPort of the uplink message. + FPort uint32 `protobuf:"varint,2,opt,name=f_port,json=fPort,proto3" json:"f_port,omitempty"` + // LoRaWAN FCntUp of the uplink message. + FCnt uint32 `protobuf:"varint,3,opt,name=f_cnt,json=fCnt,proto3" json:"f_cnt,omitempty"` // The frame payload of the uplink message. // The payload is still encrypted if the skip_payload_crypto field of the EndDevice // is true, which is indicated by the presence of the app_s_key field. @@ -496,9 +498,16 @@ type ApplicationUplink struct { DecodedPayload *types.Struct `protobuf:"bytes,5,opt,name=decoded_payload,json=decodedPayload,proto3" json:"decoded_payload,omitempty"` // Warnings generated by the message processor while decoding the frm_payload. DecodedPayloadWarnings []string `protobuf:"bytes,12,rep,name=decoded_payload_warnings,json=decodedPayloadWarnings,proto3" json:"decoded_payload_warnings,omitempty"` + // The normalized frame payload of the uplink message. + // This field is set by the message processor that is configured for the end device (see formatters) or application (see default_formatters). + // If the message processor is a custom script, there is no uplink normalizer script and the decoded output is valid + // normalized payload, this field contains the decoded payload. + NormalizedPayload []*types.Struct `protobuf:"bytes,17,rep,name=normalized_payload,json=normalizedPayload,proto3" json:"normalized_payload,omitempty"` + // Warnings generated by the message processor while normalizing the decoded payload. + NormalizedPayloadWarnings []string `protobuf:"bytes,18,rep,name=normalized_payload_warnings,json=normalizedPayloadWarnings,proto3" json:"normalized_payload_warnings,omitempty"` // A list of metadata for each antenna of each gateway that received this message. RxMetadata []*RxMetadata `protobuf:"bytes,6,rep,name=rx_metadata,json=rxMetadata,proto3" json:"rx_metadata,omitempty"` - // Settings for the transmission. + // Transmission settings used by the end device. Settings *TxSettings `protobuf:"bytes,7,opt,name=settings,proto3" json:"settings,omitempty"` // Server time when the Network Server received the message. ReceivedAt *types.Timestamp `protobuf:"bytes,8,opt,name=received_at,json=receivedAt,proto3" json:"received_at,omitempty"` @@ -512,8 +521,9 @@ type ApplicationUplink struct { // is true. // Can be used with app_s_key to encrypt downlink payloads. LastAFCntDown uint32 `protobuf:"varint,10,opt,name=last_a_f_cnt_down,json=lastAFCntDown,proto3" json:"last_a_f_cnt_down,omitempty"` - Confirmed bool `protobuf:"varint,11,opt,name=confirmed,proto3" json:"confirmed,omitempty"` - // Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the RawPayload size and the transmission settings. + // Indicates whether the end device used confirmed data uplink. + Confirmed bool `protobuf:"varint,11,opt,name=confirmed,proto3" json:"confirmed,omitempty"` + // Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings. ConsumedAirtime *types.Duration `protobuf:"bytes,13,opt,name=consumed_airtime,json=consumedAirtime,proto3" json:"consumed_airtime,omitempty"` // End device location metadata, set by the Application Server while handling the message. Locations map[string]*Location `protobuf:"bytes,14,rep,name=locations,proto3" json:"locations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` @@ -592,6 +602,20 @@ func (m *ApplicationUplink) GetDecodedPayloadWarnings() []string { return nil } +func (m *ApplicationUplink) GetNormalizedPayload() []*types.Struct { + if m != nil { + return m.NormalizedPayload + } + return nil +} + +func (m *ApplicationUplink) GetNormalizedPayloadWarnings() []string { + if m != nil { + return m.NormalizedPayloadWarnings + } + return nil +} + func (m *ApplicationUplink) GetRxMetadata() []*RxMetadata { if m != nil { return m.RxMetadata @@ -662,6 +686,164 @@ func (m *ApplicationUplink) GetNetworkIds() *NetworkIdentifiers { return nil } +type ApplicationUplinkNormalized struct { + // Join Server issued identifier for the session keys used by this uplink. + SessionKeyId []byte `protobuf:"bytes,1,opt,name=session_key_id,json=sessionKeyId,proto3" json:"session_key_id,omitempty"` + // LoRaWAN FPort of the uplink message. + FPort uint32 `protobuf:"varint,2,opt,name=f_port,json=fPort,proto3" json:"f_port,omitempty"` + // LoRaWAN FCntUp of the uplink message. + FCnt uint32 `protobuf:"varint,3,opt,name=f_cnt,json=fCnt,proto3" json:"f_cnt,omitempty"` + // The frame payload of the uplink message. + // This field is always decrypted with AppSKey. + FrmPayload []byte `protobuf:"bytes,4,opt,name=frm_payload,json=frmPayload,proto3" json:"frm_payload,omitempty"` + // The normalized frame payload of the uplink message. + // This field is set for each item in normalized_payload in the corresponding ApplicationUplink message. + NormalizedPayload *types.Struct `protobuf:"bytes,5,opt,name=normalized_payload,json=normalizedPayload,proto3" json:"normalized_payload,omitempty"` + // This field is set to normalized_payload_warnings in the corresponding ApplicationUplink message. + NormalizedPayloadWarnings []string `protobuf:"bytes,6,rep,name=normalized_payload_warnings,json=normalizedPayloadWarnings,proto3" json:"normalized_payload_warnings,omitempty"` + // A list of metadata for each antenna of each gateway that received this message. + RxMetadata []*RxMetadata `protobuf:"bytes,7,rep,name=rx_metadata,json=rxMetadata,proto3" json:"rx_metadata,omitempty"` + // Transmission settings used by the end device. + Settings *TxSettings `protobuf:"bytes,8,opt,name=settings,proto3" json:"settings,omitempty"` + // Server time when the Network Server received the message. + ReceivedAt *types.Timestamp `protobuf:"bytes,9,opt,name=received_at,json=receivedAt,proto3" json:"received_at,omitempty"` + // Indicates whether the end device used confirmed data uplink. + Confirmed bool `protobuf:"varint,10,opt,name=confirmed,proto3" json:"confirmed,omitempty"` + // Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings. + ConsumedAirtime *types.Duration `protobuf:"bytes,11,opt,name=consumed_airtime,json=consumedAirtime,proto3" json:"consumed_airtime,omitempty"` + // End device location metadata, set by the Application Server while handling the message. + Locations map[string]*Location `protobuf:"bytes,12,rep,name=locations,proto3" json:"locations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // End device version identifiers, set by the Application Server while handling the message. + VersionIds *EndDeviceVersionIdentifiers `protobuf:"bytes,13,opt,name=version_ids,json=versionIds,proto3" json:"version_ids,omitempty"` + // Network identifiers, set by the Network Server that handles the message. + NetworkIds *NetworkIdentifiers `protobuf:"bytes,14,opt,name=network_ids,json=networkIds,proto3" json:"network_ids,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ApplicationUplinkNormalized) Reset() { *m = ApplicationUplinkNormalized{} } +func (m *ApplicationUplinkNormalized) String() string { return proto.CompactTextString(m) } +func (*ApplicationUplinkNormalized) ProtoMessage() {} +func (*ApplicationUplinkNormalized) Descriptor() ([]byte, []int) { + return fileDescriptor_bbc6bff5780bdc9d, []int{6} +} +func (m *ApplicationUplinkNormalized) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ApplicationUplinkNormalized.Unmarshal(m, b) +} +func (m *ApplicationUplinkNormalized) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ApplicationUplinkNormalized.Marshal(b, m, deterministic) +} +func (m *ApplicationUplinkNormalized) XXX_Merge(src proto.Message) { + xxx_messageInfo_ApplicationUplinkNormalized.Merge(m, src) +} +func (m *ApplicationUplinkNormalized) XXX_Size() int { + return xxx_messageInfo_ApplicationUplinkNormalized.Size(m) +} +func (m *ApplicationUplinkNormalized) XXX_DiscardUnknown() { + xxx_messageInfo_ApplicationUplinkNormalized.DiscardUnknown(m) +} + +var xxx_messageInfo_ApplicationUplinkNormalized proto.InternalMessageInfo + +func (m *ApplicationUplinkNormalized) GetSessionKeyId() []byte { + if m != nil { + return m.SessionKeyId + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetFPort() uint32 { + if m != nil { + return m.FPort + } + return 0 +} + +func (m *ApplicationUplinkNormalized) GetFCnt() uint32 { + if m != nil { + return m.FCnt + } + return 0 +} + +func (m *ApplicationUplinkNormalized) GetFrmPayload() []byte { + if m != nil { + return m.FrmPayload + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetNormalizedPayload() *types.Struct { + if m != nil { + return m.NormalizedPayload + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetNormalizedPayloadWarnings() []string { + if m != nil { + return m.NormalizedPayloadWarnings + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetRxMetadata() []*RxMetadata { + if m != nil { + return m.RxMetadata + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetSettings() *TxSettings { + if m != nil { + return m.Settings + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetReceivedAt() *types.Timestamp { + if m != nil { + return m.ReceivedAt + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetConfirmed() bool { + if m != nil { + return m.Confirmed + } + return false +} + +func (m *ApplicationUplinkNormalized) GetConsumedAirtime() *types.Duration { + if m != nil { + return m.ConsumedAirtime + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetLocations() map[string]*Location { + if m != nil { + return m.Locations + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetVersionIds() *EndDeviceVersionIdentifiers { + if m != nil { + return m.VersionIds + } + return nil +} + +func (m *ApplicationUplinkNormalized) GetNetworkIds() *NetworkIdentifiers { + if m != nil { + return m.NetworkIds + } + return nil +} + type ApplicationLocation struct { Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` Location *Location `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` @@ -675,7 +857,7 @@ func (m *ApplicationLocation) Reset() { *m = ApplicationLocation{} } func (m *ApplicationLocation) String() string { return proto.CompactTextString(m) } func (*ApplicationLocation) ProtoMessage() {} func (*ApplicationLocation) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{6} + return fileDescriptor_bbc6bff5780bdc9d, []int{7} } func (m *ApplicationLocation) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationLocation.Unmarshal(m, b) @@ -737,7 +919,7 @@ func (m *ApplicationJoinAccept) Reset() { *m = ApplicationJoinAccept{} } func (m *ApplicationJoinAccept) String() string { return proto.CompactTextString(m) } func (*ApplicationJoinAccept) ProtoMessage() {} func (*ApplicationJoinAccept) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{7} + return fileDescriptor_bbc6bff5780bdc9d, []int{8} } func (m *ApplicationJoinAccept) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationJoinAccept.Unmarshal(m, b) @@ -825,7 +1007,7 @@ func (m *ApplicationDownlink) Reset() { *m = ApplicationDownlink{} } func (m *ApplicationDownlink) String() string { return proto.CompactTextString(m) } func (*ApplicationDownlink) ProtoMessage() {} func (*ApplicationDownlink) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{8} + return fileDescriptor_bbc6bff5780bdc9d, []int{9} } func (m *ApplicationDownlink) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationDownlink.Unmarshal(m, b) @@ -936,7 +1118,7 @@ func (m *ApplicationDownlink_ClassBC) Reset() { *m = ApplicationDownlink func (m *ApplicationDownlink_ClassBC) String() string { return proto.CompactTextString(m) } func (*ApplicationDownlink_ClassBC) ProtoMessage() {} func (*ApplicationDownlink_ClassBC) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{8, 0} + return fileDescriptor_bbc6bff5780bdc9d, []int{9, 0} } func (m *ApplicationDownlink_ClassBC) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationDownlink_ClassBC.Unmarshal(m, b) @@ -981,7 +1163,7 @@ func (m *ApplicationDownlinks) Reset() { *m = ApplicationDownlinks{} } func (m *ApplicationDownlinks) String() string { return proto.CompactTextString(m) } func (*ApplicationDownlinks) ProtoMessage() {} func (*ApplicationDownlinks) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{9} + return fileDescriptor_bbc6bff5780bdc9d, []int{10} } func (m *ApplicationDownlinks) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationDownlinks.Unmarshal(m, b) @@ -1020,7 +1202,7 @@ func (m *ApplicationDownlinkFailed) Reset() { *m = ApplicationDownlinkFa func (m *ApplicationDownlinkFailed) String() string { return proto.CompactTextString(m) } func (*ApplicationDownlinkFailed) ProtoMessage() {} func (*ApplicationDownlinkFailed) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{10} + return fileDescriptor_bbc6bff5780bdc9d, []int{11} } func (m *ApplicationDownlinkFailed) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationDownlinkFailed.Unmarshal(m, b) @@ -1067,7 +1249,7 @@ func (m *ApplicationInvalidatedDownlinks) Reset() { *m = ApplicationInva func (m *ApplicationInvalidatedDownlinks) String() string { return proto.CompactTextString(m) } func (*ApplicationInvalidatedDownlinks) ProtoMessage() {} func (*ApplicationInvalidatedDownlinks) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{11} + return fileDescriptor_bbc6bff5780bdc9d, []int{12} } func (m *ApplicationInvalidatedDownlinks) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationInvalidatedDownlinks.Unmarshal(m, b) @@ -1124,7 +1306,7 @@ func (m *DownlinkQueueOperationErrorDetails) Reset() { *m = DownlinkQueu func (m *DownlinkQueueOperationErrorDetails) String() string { return proto.CompactTextString(m) } func (*DownlinkQueueOperationErrorDetails) ProtoMessage() {} func (*DownlinkQueueOperationErrorDetails) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{12} + return fileDescriptor_bbc6bff5780bdc9d, []int{13} } func (m *DownlinkQueueOperationErrorDetails) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DownlinkQueueOperationErrorDetails.Unmarshal(m, b) @@ -1198,7 +1380,7 @@ func (m *ApplicationServiceData) Reset() { *m = ApplicationServiceData{} func (m *ApplicationServiceData) String() string { return proto.CompactTextString(m) } func (*ApplicationServiceData) ProtoMessage() {} func (*ApplicationServiceData) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{13} + return fileDescriptor_bbc6bff5780bdc9d, []int{14} } func (m *ApplicationServiceData) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationServiceData.Unmarshal(m, b) @@ -1240,6 +1422,7 @@ type ApplicationUp struct { ReceivedAt *types.Timestamp `protobuf:"bytes,12,opt,name=received_at,json=receivedAt,proto3" json:"received_at,omitempty"` // Types that are valid to be assigned to Up: // *ApplicationUp_UplinkMessage + // *ApplicationUp_UplinkNormalized // *ApplicationUp_JoinAccept // *ApplicationUp_DownlinkAck // *ApplicationUp_DownlinkNack @@ -1261,7 +1444,7 @@ func (m *ApplicationUp) Reset() { *m = ApplicationUp{} } func (m *ApplicationUp) String() string { return proto.CompactTextString(m) } func (*ApplicationUp) ProtoMessage() {} func (*ApplicationUp) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{14} + return fileDescriptor_bbc6bff5780bdc9d, []int{15} } func (m *ApplicationUp) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ApplicationUp.Unmarshal(m, b) @@ -1288,6 +1471,9 @@ type isApplicationUp_Up interface { type ApplicationUp_UplinkMessage struct { UplinkMessage *ApplicationUplink `protobuf:"bytes,3,opt,name=uplink_message,json=uplinkMessage,proto3,oneof" json:"uplink_message,omitempty"` } +type ApplicationUp_UplinkNormalized struct { + UplinkNormalized *ApplicationUplinkNormalized `protobuf:"bytes,15,opt,name=uplink_normalized,json=uplinkNormalized,proto3,oneof" json:"uplink_normalized,omitempty"` +} type ApplicationUp_JoinAccept struct { JoinAccept *ApplicationJoinAccept `protobuf:"bytes,4,opt,name=join_accept,json=joinAccept,proto3,oneof" json:"join_accept,omitempty"` } @@ -1317,6 +1503,7 @@ type ApplicationUp_ServiceData struct { } func (*ApplicationUp_UplinkMessage) isApplicationUp_Up() {} +func (*ApplicationUp_UplinkNormalized) isApplicationUp_Up() {} func (*ApplicationUp_JoinAccept) isApplicationUp_Up() {} func (*ApplicationUp_DownlinkAck) isApplicationUp_Up() {} func (*ApplicationUp_DownlinkNack) isApplicationUp_Up() {} @@ -1362,6 +1549,13 @@ func (m *ApplicationUp) GetUplinkMessage() *ApplicationUplink { return nil } +func (m *ApplicationUp) GetUplinkNormalized() *ApplicationUplinkNormalized { + if x, ok := m.GetUp().(*ApplicationUp_UplinkNormalized); ok { + return x.UplinkNormalized + } + return nil +} + func (m *ApplicationUp) GetJoinAccept() *ApplicationJoinAccept { if x, ok := m.GetUp().(*ApplicationUp_JoinAccept); ok { return x.JoinAccept @@ -1436,6 +1630,7 @@ func (m *ApplicationUp) GetSimulated() bool { func (*ApplicationUp) XXX_OneofWrappers() []interface{} { return []interface{}{ (*ApplicationUp_UplinkMessage)(nil), + (*ApplicationUp_UplinkNormalized)(nil), (*ApplicationUp_JoinAccept)(nil), (*ApplicationUp_DownlinkAck)(nil), (*ApplicationUp_DownlinkNack)(nil), @@ -1466,7 +1661,7 @@ func (m *MessagePayloadFormatters) Reset() { *m = MessagePayloadFormatte func (m *MessagePayloadFormatters) String() string { return proto.CompactTextString(m) } func (*MessagePayloadFormatters) ProtoMessage() {} func (*MessagePayloadFormatters) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{15} + return fileDescriptor_bbc6bff5780bdc9d, []int{16} } func (m *MessagePayloadFormatters) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_MessagePayloadFormatters.Unmarshal(m, b) @@ -1526,7 +1721,7 @@ func (m *DownlinkQueueRequest) Reset() { *m = DownlinkQueueRequest{} } func (m *DownlinkQueueRequest) String() string { return proto.CompactTextString(m) } func (*DownlinkQueueRequest) ProtoMessage() {} func (*DownlinkQueueRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_bbc6bff5780bdc9d, []int{16} + return fileDescriptor_bbc6bff5780bdc9d, []int{17} } func (m *DownlinkQueueRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DownlinkQueueRequest.Unmarshal(m, b) @@ -1579,6 +1774,10 @@ func init() { golang_proto.RegisterType((*ApplicationUplink)(nil), "ttn.lorawan.v3.ApplicationUplink") proto.RegisterMapType((map[string]*Location)(nil), "ttn.lorawan.v3.ApplicationUplink.LocationsEntry") golang_proto.RegisterMapType((map[string]*Location)(nil), "ttn.lorawan.v3.ApplicationUplink.LocationsEntry") + proto.RegisterType((*ApplicationUplinkNormalized)(nil), "ttn.lorawan.v3.ApplicationUplinkNormalized") + golang_proto.RegisterType((*ApplicationUplinkNormalized)(nil), "ttn.lorawan.v3.ApplicationUplinkNormalized") + proto.RegisterMapType((map[string]*Location)(nil), "ttn.lorawan.v3.ApplicationUplinkNormalized.LocationsEntry") + golang_proto.RegisterMapType((map[string]*Location)(nil), "ttn.lorawan.v3.ApplicationUplinkNormalized.LocationsEntry") proto.RegisterType((*ApplicationLocation)(nil), "ttn.lorawan.v3.ApplicationLocation") golang_proto.RegisterType((*ApplicationLocation)(nil), "ttn.lorawan.v3.ApplicationLocation") proto.RegisterMapType((map[string]string)(nil), "ttn.lorawan.v3.ApplicationLocation.AttributesEntry") @@ -1613,168 +1812,179 @@ func init() { } var fileDescriptor_bbc6bff5780bdc9d = []byte{ - // 2603 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x59, 0x4d, 0x8c, 0x23, 0x47, - 0xf5, 0x9f, 0xb6, 0x3d, 0xfe, 0x78, 0xfe, 0x98, 0xde, 0xca, 0x64, 0xd3, 0x33, 0xff, 0x4d, 0x76, - 0xfe, 0xce, 0x86, 0x4c, 0x36, 0x8c, 0xbd, 0xcc, 0xb2, 0x24, 0x2c, 0x84, 0xc5, 0xf6, 0x78, 0x77, - 0x3c, 0x1f, 0xb6, 0x53, 0xf6, 0x6e, 0x76, 0x89, 0x42, 0xab, 0xc7, 0x5d, 0xe3, 0xe9, 0x8c, 0xdd, - 0xdd, 0xe9, 0x2e, 0x7b, 0xc6, 0x41, 0x48, 0x51, 0x6e, 0x44, 0xe2, 0x40, 0x2e, 0x88, 0x80, 0x10, - 0x02, 0x29, 0x87, 0x15, 0xe2, 0x8c, 0xc4, 0x25, 0x17, 0xa4, 0x08, 0xc1, 0x99, 0x2b, 0x08, 0x4e, - 0x91, 0xb8, 0xe4, 0x84, 0xe6, 0x12, 0xd4, 0xd5, 0xd5, 0x76, 0x77, 0xdb, 0x99, 0x9d, 0x49, 0x08, - 0x82, 0xcb, 0xc8, 0x5d, 0xf5, 0xde, 0xaf, 0x5e, 0xbd, 0xf7, 0xea, 0xfd, 0x5e, 0xd5, 0xc0, 0x4a, - 0xcf, 0xb0, 0x94, 0x23, 0x45, 0x5f, 0xb3, 0xa9, 0xd2, 0x39, 0x2c, 0x2a, 0xa6, 0x56, 0xec, 0x13, - 0xdb, 0x56, 0xba, 0xc4, 0x2e, 0x98, 0x96, 0x41, 0x0d, 0x94, 0xa3, 0x54, 0x2f, 0x70, 0xa9, 0xc2, - 0xf0, 0xfa, 0x72, 0xa9, 0xab, 0xd1, 0x83, 0xc1, 0x5e, 0xa1, 0x63, 0xf4, 0x8b, 0x44, 0x1f, 0x1a, - 0x23, 0xd3, 0x32, 0x8e, 0x47, 0x45, 0x26, 0xdc, 0x59, 0xeb, 0x12, 0x7d, 0x6d, 0xa8, 0xf4, 0x34, - 0x55, 0xa1, 0xa4, 0x38, 0xf5, 0xc3, 0x85, 0x5c, 0x5e, 0xf3, 0x41, 0x74, 0x8d, 0xae, 0xe1, 0x2a, - 0xef, 0x0d, 0xf6, 0xd9, 0x17, 0xfb, 0x60, 0xbf, 0xb8, 0x78, 0xc5, 0x27, 0xde, 0x3e, 0x20, 0xed, - 0x03, 0x4d, 0xef, 0xda, 0x35, 0x5d, 0x1d, 0xd8, 0xd4, 0xd2, 0x88, 0xed, 0x5f, 0xba, 0x6b, 0xac, - 0xbd, 0x6e, 0x1b, 0x7a, 0x51, 0xd1, 0x75, 0x83, 0x2a, 0x54, 0x33, 0x74, 0xbe, 0x8d, 0xe5, 0x8d, - 0x73, 0x81, 0xec, 0xf7, 0x94, 0xae, 0x3d, 0x03, 0xe5, 0x52, 0xd7, 0x30, 0xba, 0x3d, 0x32, 0x31, - 0xd8, 0xa6, 0xd6, 0xa0, 0x43, 0xf9, 0xec, 0xe5, 0xf0, 0x2c, 0xd5, 0xfa, 0xc4, 0xa6, 0x4a, 0xdf, - 0xe4, 0x02, 0x4f, 0x85, 0x05, 0xd4, 0x81, 0xc5, 0xf0, 0xf9, 0xfc, 0x93, 0xd3, 0xd1, 0x20, 0x96, - 0x65, 0x58, 0x7c, 0xfa, 0xe9, 0xe9, 0x69, 0x4d, 0x25, 0x3a, 0xd5, 0xf6, 0x35, 0x62, 0x8d, 0x4d, - 0x9c, 0x16, 0x3a, 0x24, 0x23, 0x6f, 0xf6, 0xf2, 0xf4, 0xac, 0x17, 0x5b, 0x57, 0x60, 0x66, 0x42, - 0x50, 0x45, 0x55, 0xa8, 0xc2, 0x25, 0x0a, 0x3e, 0x57, 0x19, 0x26, 0xd1, 0x15, 0x53, 0x1b, 0xae, - 0x17, 0x0d, 0x93, 0xf9, 0x69, 0xda, 0x67, 0xf9, 0xbf, 0x47, 0x21, 0x7b, 0xd7, 0xec, 0x69, 0xfa, - 0xe1, 0xae, 0x9b, 0x59, 0xe8, 0x32, 0xa4, 0x2d, 0xe5, 0x48, 0x36, 0x95, 0x51, 0xcf, 0x50, 0x54, - 0x49, 0x58, 0x11, 0x56, 0x33, 0x18, 0x2c, 0xe5, 0xa8, 0xe9, 0x8e, 0xa0, 0xaf, 0x40, 0xc2, 0x9b, - 0x8c, 0xac, 0x08, 0xab, 0xe9, 0xf5, 0x27, 0x0a, 0xc1, 0x2c, 0x2c, 0x70, 0x28, 0xec, 0xc9, 0xa1, - 0x6f, 0x43, 0xd2, 0x26, 0x94, 0x3a, 0x51, 0x95, 0x62, 0x4c, 0x67, 0x39, 0xac, 0xd3, 0x3e, 0x6e, - 0x71, 0x89, 0x72, 0xf2, 0xa4, 0x3c, 0xff, 0x8e, 0x10, 0x11, 0x05, 0x3c, 0xd6, 0x42, 0xdf, 0x80, - 0xb4, 0x75, 0x2c, 0x7b, 0x9b, 0x95, 0xe6, 0x57, 0xa2, 0xb3, 0x40, 0xf0, 0xf1, 0x2e, 0x97, 0xc0, - 0x60, 0x8d, 0x7f, 0x33, 0x65, 0xd2, 0x21, 0xda, 0x90, 0xa8, 0xb2, 0x42, 0xa5, 0x38, 0xb7, 0xc0, - 0x8d, 0x77, 0xc1, 0x8b, 0x77, 0xa1, 0xed, 0x25, 0x04, 0x06, 0x4f, 0xbc, 0x44, 0xd1, 0x0d, 0x58, - 0xe8, 0x18, 0x96, 0x45, 0x7a, 0xcc, 0x6f, 0xb2, 0xa6, 0xda, 0x52, 0x62, 0x25, 0xba, 0x9a, 0x2a, - 0x67, 0x4e, 0xca, 0xa9, 0x77, 0x85, 0x78, 0x3e, 0x66, 0x45, 0x24, 0x15, 0xe7, 0x7c, 0x42, 0x35, - 0xd5, 0x46, 0x37, 0x61, 0x51, 0x25, 0x43, 0xad, 0x43, 0xe4, 0xce, 0x81, 0xa2, 0xeb, 0xa4, 0x27, - 0x6b, 0xba, 0x4a, 0x8e, 0xa5, 0xd4, 0x8a, 0xb0, 0x9a, 0x65, 0x5b, 0xbc, 0x1a, 0x95, 0x3e, 0x11, - 0x30, 0x72, 0xa5, 0x2a, 0xae, 0x50, 0xcd, 0x91, 0x41, 0x1b, 0x20, 0x76, 0x0c, 0xdd, 0x1e, 0xf4, - 0x1d, 0x7b, 0x35, 0xcb, 0x49, 0x54, 0x09, 0x98, 0xd1, 0x4b, 0x53, 0x46, 0x6f, 0xf0, 0x24, 0xc5, - 0x0b, 0x9e, 0x4a, 0xc9, 0xd5, 0xd8, 0x8a, 0x25, 0xa3, 0x62, 0x6c, 0x2b, 0x96, 0x4c, 0x8a, 0xa9, - 0xfc, 0xcf, 0xa2, 0xb0, 0xb0, 0x61, 0x1c, 0xe9, 0x5f, 0x74, 0xa0, 0xb7, 0x20, 0x47, 0x74, 0x55, - 0xe6, 0x3b, 0x77, 0x7c, 0x15, 0x65, 0x9a, 0x57, 0xc2, 0x9a, 0x55, 0x5d, 0xdd, 0x60, 0x42, 0xb5, - 0xc9, 0x19, 0xc1, 0x19, 0x32, 0x19, 0xb5, 0xd1, 0x0d, 0x48, 0x58, 0xe4, 0x8d, 0x01, 0xb1, 0x29, - 0xcf, 0x99, 0xa5, 0xe9, 0x9c, 0xc1, 0xae, 0xc0, 0xe6, 0x1c, 0xf6, 0x64, 0xd1, 0x4d, 0x48, 0xd9, - 0x9d, 0x03, 0xa2, 0x0e, 0x7a, 0x44, 0x95, 0xe6, 0x1f, 0x95, 0x6c, 0x9b, 0x73, 0x78, 0x22, 0x3e, - 0x2b, 0xd6, 0xf1, 0x33, 0xc4, 0xba, 0x00, 0x39, 0x9b, 0xd8, 0xb6, 0xa3, 0x72, 0x48, 0x46, 0xb2, - 0xa6, 0x4a, 0x09, 0xc7, 0x99, 0x2c, 0xca, 0x6f, 0x46, 0xa5, 0xb7, 0x44, 0x9c, 0xe1, 0xf3, 0xdb, - 0x64, 0x54, 0x53, 0xcb, 0x0b, 0x93, 0xe3, 0x80, 0xa2, 0xff, 0x2c, 0x0b, 0xf9, 0x1f, 0x44, 0x41, - 0x6c, 0x1f, 0x97, 0x3a, 0x87, 0xba, 0x71, 0xd4, 0x23, 0x6a, 0xb7, 0x4f, 0xf4, 0x99, 0x89, 0x27, - 0x9c, 0xc1, 0x98, 0x1a, 0xc4, 0x2d, 0x62, 0x0f, 0x7a, 0x94, 0x05, 0x2d, 0xb7, 0xfe, 0xec, 0xf4, - 0xe6, 0x83, 0x0b, 0x15, 0x30, 0x13, 0x67, 0xd6, 0xbe, 0xcd, 0x8e, 0x1d, 0x07, 0x40, 0x5b, 0x20, - 0xaa, 0x3c, 0x69, 0x64, 0x4e, 0x3c, 0x3c, 0x9e, 0x97, 0xc3, 0xa0, 0xa1, 0xe4, 0xc2, 0x0b, 0x6a, - 0x70, 0x20, 0xff, 0xbe, 0x00, 0x71, 0x77, 0x21, 0x94, 0x86, 0x44, 0xeb, 0x6e, 0xa5, 0x52, 0x6d, - 0xb5, 0xc4, 0x39, 0x74, 0x01, 0xb2, 0x77, 0xeb, 0xdb, 0xf5, 0xc6, 0x2b, 0x75, 0xb9, 0x8a, 0x71, - 0x03, 0x8b, 0x02, 0xca, 0x40, 0xb2, 0xdd, 0x68, 0xc8, 0x3b, 0xa5, 0x76, 0x55, 0x8c, 0xa0, 0x2c, - 0xa4, 0x9c, 0xaf, 0x6a, 0x09, 0xef, 0x3c, 0x10, 0xa3, 0x68, 0x11, 0xc4, 0x4a, 0x63, 0x67, 0xa7, - 0xd6, 0xaa, 0x35, 0xea, 0x72, 0xb3, 0x54, 0xd9, 0xae, 0xb6, 0xc5, 0x58, 0x70, 0xb4, 0x5c, 0x2d, - 0x55, 0x1a, 0x75, 0x71, 0xde, 0x59, 0xa8, 0x7d, 0x5f, 0xbe, 0x8d, 0xab, 0x2f, 0x8b, 0x71, 0x86, - 0x7a, 0x5f, 0x6e, 0x36, 0x5e, 0xa9, 0x62, 0x31, 0x81, 0x44, 0xc8, 0xdc, 0x69, 0xb6, 0xe4, 0xbb, - 0xf5, 0x9d, 0x46, 0x65, 0xbb, 0xba, 0x21, 0x26, 0x97, 0xe3, 0x1f, 0x3d, 0x5c, 0x8a, 0x48, 0x42, - 0xfe, 0xc7, 0x02, 0x3c, 0x71, 0x47, 0xa1, 0xe4, 0x48, 0x19, 0x4d, 0x85, 0xa4, 0x02, 0xe9, 0xae, - 0x3b, 0xc5, 0xc3, 0xe1, 0xf8, 0x22, 0x1f, 0xf6, 0x05, 0xd7, 0xf6, 0x67, 0x36, 0x74, 0xbd, 0x31, - 0x1b, 0xbd, 0x00, 0x71, 0x7a, 0x2c, 0x2b, 0x9d, 0x43, 0x7e, 0xaa, 0x56, 0x1e, 0x15, 0x20, 0x3c, - 0x4f, 0x9d, 0x91, 0xbc, 0x05, 0x8b, 0x1c, 0x3a, 0x58, 0xb1, 0x4b, 0x90, 0xf0, 0xa2, 0xe3, 0x5a, - 0xf4, 0x64, 0x18, 0x31, 0x20, 0xef, 0xab, 0xaf, 0x9e, 0x1e, 0x7a, 0x02, 0x12, 0x7b, 0x8a, 0xae, - 0x3a, 0xa9, 0xeb, 0x18, 0x95, 0xc2, 0x71, 0xe7, 0xb3, 0xa6, 0xe6, 0xff, 0x9a, 0x80, 0x0b, 0x25, - 0xd3, 0xec, 0x69, 0x1d, 0x96, 0x60, 0x2e, 0xd0, 0x8c, 0x84, 0x17, 0x4e, 0x4b, 0x78, 0x94, 0x87, - 0xf8, 0xbe, 0x6c, 0x1a, 0x96, 0x9b, 0x93, 0xd9, 0x72, 0xfa, 0xa4, 0x9c, 0xbc, 0x1a, 0x97, 0x3e, - 0x11, 0x5e, 0xfc, 0x8b, 0x80, 0xe7, 0xf7, 0x9b, 0x86, 0x45, 0xd1, 0x63, 0x30, 0xbf, 0x2f, 0x77, - 0x74, 0xca, 0x32, 0x2c, 0x8b, 0x63, 0xfb, 0x15, 0x9d, 0x3a, 0x35, 0x6a, 0xdf, 0xea, 0x8f, 0x6b, - 0x54, 0xcc, 0xad, 0x51, 0xfb, 0x56, 0xbf, 0x39, 0x66, 0x96, 0x05, 0x95, 0x74, 0x0c, 0x95, 0xa8, - 0x63, 0xa1, 0x79, 0x5e, 0xab, 0xc2, 0x95, 0xb2, 0xc5, 0xba, 0x01, 0x9c, 0xe3, 0xf2, 0x1e, 0xc2, - 0x8b, 0x20, 0x85, 0x10, 0xe4, 0x23, 0xc5, 0xd2, 0x19, 0x57, 0x65, 0x9c, 0xf3, 0x86, 0x2f, 0x06, - 0x35, 0x5e, 0xe1, 0xb3, 0x61, 0x4e, 0x8a, 0x9f, 0x8b, 0x93, 0xfc, 0x94, 0x98, 0xf8, 0xcc, 0x94, - 0xe8, 0x63, 0xb5, 0xe4, 0xb9, 0x58, 0xed, 0x05, 0x48, 0x29, 0xa6, 0x29, 0xdb, 0x4e, 0xfc, 0x18, - 0x27, 0xa5, 0xd7, 0xff, 0x2f, 0xbc, 0xfe, 0x36, 0x19, 0x55, 0xf5, 0x21, 0xe9, 0x19, 0x26, 0xc1, - 0x09, 0xc5, 0x34, 0x5b, 0xdb, 0x64, 0x84, 0x56, 0xe1, 0x42, 0x4f, 0xb1, 0xa9, 0xac, 0xc8, 0x2c, - 0x5a, 0xb2, 0x73, 0xce, 0x19, 0x39, 0x65, 0x71, 0xd6, 0x99, 0x28, 0xdd, 0xae, 0xe8, 0xd4, 0xa9, - 0x06, 0xe8, 0x12, 0xa4, 0x3a, 0x86, 0xbe, 0xaf, 0x59, 0x7d, 0xa2, 0x4a, 0xe9, 0x15, 0x61, 0x35, - 0x89, 0x27, 0x03, 0x33, 0x39, 0x2e, 0x7b, 0x5e, 0x8e, 0x43, 0x75, 0x48, 0xf5, 0x0c, 0x37, 0x35, - 0x6d, 0x29, 0xc7, 0x02, 0x70, 0x2d, 0xbc, 0x8d, 0xa9, 0xf4, 0x2d, 0xec, 0x78, 0x2a, 0x55, 0x9d, - 0x5a, 0x23, 0x3c, 0x81, 0x40, 0x3b, 0x90, 0x1e, 0x12, 0xcb, 0xf6, 0xea, 0xed, 0x02, 0x33, 0xe8, - 0xf9, 0x4f, 0x25, 0xaf, 0x7b, 0xae, 0x6c, 0xe0, 0xa4, 0x0f, 0xbd, 0x31, 0xdb, 0x29, 0x17, 0x3a, - 0xa1, 0x47, 0x86, 0x75, 0xc8, 0xd0, 0xc4, 0xd9, 0xe5, 0xa2, 0xee, 0x8a, 0x04, 0x40, 0x74, 0x6f, - 0xcc, 0x5e, 0xbe, 0x07, 0xb9, 0xa0, 0xbd, 0x48, 0x84, 0xa8, 0x13, 0x35, 0x81, 0x1d, 0x54, 0xe7, - 0x27, 0x2a, 0xc0, 0xfc, 0x50, 0xe9, 0x0d, 0x08, 0xaf, 0x28, 0x52, 0x78, 0x09, 0x0f, 0x00, 0xbb, - 0x62, 0x37, 0x23, 0x2f, 0x0a, 0x37, 0x93, 0x1f, 0x3f, 0x5c, 0x8a, 0x25, 0x05, 0x71, 0x2e, 0xff, - 0xfb, 0x08, 0x3c, 0xe6, 0x73, 0x92, 0x27, 0x8c, 0x24, 0x48, 0xd8, 0xc4, 0x72, 0xf6, 0xc9, 0xd7, - 0xf2, 0x3e, 0xd1, 0xb7, 0x20, 0xe9, 0xf9, 0xec, 0x51, 0x4b, 0xfa, 0x53, 0xd7, 0xd3, 0x41, 0xef, - 0x08, 0x00, 0x0a, 0xa5, 0x96, 0xb6, 0x37, 0xa0, 0xc4, 0xe9, 0x11, 0x9c, 0xc0, 0x5d, 0x3f, 0x25, - 0x70, 0x1e, 0x5a, 0xa1, 0x34, 0xd6, 0x62, 0xbe, 0x28, 0xdf, 0x38, 0x29, 0xaf, 0xbf, 0x27, 0x14, - 0x45, 0xc8, 0x5f, 0xb1, 0xf2, 0xd2, 0x95, 0xf5, 0xa7, 0xbe, 0xfb, 0xaa, 0xb2, 0xf6, 0xe6, 0xb5, - 0xb5, 0xaf, 0xbf, 0xb6, 0x7a, 0xeb, 0xe6, 0xab, 0x6b, 0xaf, 0xdd, 0xf2, 0x3e, 0x9f, 0xfb, 0xde, - 0xfa, 0x97, 0xbf, 0x7f, 0xe5, 0xea, 0xbc, 0x15, 0x95, 0x3e, 0x14, 0xb0, 0x6f, 0xf5, 0xe5, 0x97, - 0x60, 0x21, 0x84, 0x3a, 0xc3, 0xc3, 0x8b, 0x7e, 0x0f, 0xa7, 0x66, 0xfb, 0xf1, 0x0f, 0x11, 0x78, - 0xdc, 0x67, 0xf3, 0x96, 0xa1, 0xe9, 0xa5, 0x4e, 0x87, 0x98, 0xf4, 0xdc, 0xf5, 0x32, 0x70, 0x3a, - 0x23, 0xe7, 0x38, 0x9d, 0xf7, 0xe1, 0x71, 0x4d, 0xf7, 0x2e, 0x74, 0xaa, 0xec, 0x91, 0xb0, 0xe7, - 0xe2, 0xa7, 0x4f, 0x71, 0xb1, 0xc7, 0xe0, 0x78, 0xd1, 0x87, 0xe0, 0x0d, 0xda, 0xe8, 0x59, 0x58, - 0x30, 0x89, 0xae, 0x6a, 0x7a, 0x57, 0xe6, 0xa6, 0xb2, 0x6a, 0x9c, 0xc4, 0x39, 0x3e, 0xdc, 0x72, - 0x47, 0x3f, 0x57, 0x59, 0xf2, 0x39, 0xf3, 0x1f, 0x89, 0x40, 0x52, 0x7a, 0x86, 0xa0, 0x3f, 0x09, - 0x9f, 0xe2, 0xcb, 0xf7, 0x05, 0xcf, 0x99, 0x1f, 0x3f, 0x5c, 0xfa, 0x89, 0xb0, 0x5c, 0xff, 0x0c, - 0x77, 0x4a, 0xf6, 0xd7, 0xec, 0x0d, 0xba, 0x9a, 0x5e, 0xa8, 0x93, 0xa3, 0x4d, 0x72, 0x5c, 0x1e, - 0x51, 0x62, 0xdf, 0xee, 0x29, 0xdd, 0xfc, 0x9d, 0xcf, 0x89, 0x77, 0x87, 0x50, 0x06, 0xf6, 0xef, - 0xa2, 0xc6, 0x0f, 0x84, 0x19, 0xdc, 0x58, 0xfe, 0xb9, 0xf0, 0x5f, 0xbe, 0xfb, 0xff, 0x1c, 0x79, - 0xc3, 0xa9, 0xe4, 0x1d, 0x60, 0xa7, 0x78, 0x98, 0x9d, 0xee, 0x40, 0xaa, 0xd3, 0x53, 0x6c, 0x5b, - 0xde, 0x93, 0x3b, 0x9c, 0x9e, 0x9f, 0x3f, 0xc3, 0xd9, 0x29, 0x54, 0x1c, 0xa5, 0x72, 0x05, 0x27, - 0x3a, 0xee, 0x0f, 0xb4, 0x09, 0x49, 0xd3, 0xd2, 0x0c, 0x4b, 0xa3, 0x23, 0x76, 0x14, 0x72, 0xd3, - 0xf5, 0xbf, 0x7d, 0xdc, 0xe2, 0x17, 0x90, 0x26, 0x97, 0xf4, 0xb5, 0xe2, 0x63, 0xed, 0x59, 0xd7, - 0x81, 0xd4, 0xa3, 0xaf, 0x03, 0xcb, 0x3f, 0x15, 0x20, 0xc1, 0xad, 0x42, 0x55, 0x48, 0xf2, 0x3e, - 0xd4, 0xbd, 0xc3, 0xa6, 0xd7, 0x9f, 0x0b, 0x1b, 0xc3, 0x45, 0x67, 0xb4, 0xb0, 0x63, 0x55, 0x74, - 0x0b, 0xb2, 0xca, 0x9e, 0x6d, 0xf4, 0x06, 0x94, 0xc8, 0x8c, 0xb7, 0x1f, 0x7d, 0xc6, 0x33, 0x9e, - 0x82, 0x33, 0x34, 0x3e, 0xe5, 0x93, 0xe2, 0x29, 0xe4, 0x1f, 0xc0, 0xe2, 0x0c, 0x87, 0xda, 0xa8, - 0x04, 0xa9, 0x49, 0x15, 0x13, 0xce, 0x5e, 0xc5, 0x26, 0x5a, 0xf9, 0xdf, 0x08, 0xb0, 0x34, 0x43, - 0xe4, 0xb6, 0xa2, 0x39, 0x77, 0xbe, 0x1a, 0x24, 0x3d, 0x51, 0xde, 0x3e, 0x9f, 0x05, 0xdf, 0x4f, - 0x6b, 0x9e, 0x3a, 0xfa, 0x26, 0xcc, 0xb3, 0x17, 0x21, 0x5e, 0xb2, 0x2f, 0x4d, 0xf5, 0x0d, 0xce, - 0xe4, 0x06, 0xa1, 0x8a, 0xd6, 0xf3, 0xb7, 0x74, 0xae, 0x92, 0x9f, 0x90, 0x05, 0xb8, 0xec, 0x5b, - 0xb3, 0x36, 0xab, 0x1e, 0x7f, 0x7e, 0xbf, 0xa0, 0x67, 0x60, 0x81, 0xb5, 0x72, 0xbe, 0x46, 0x8e, - 0xd5, 0x20, 0x9c, 0x71, 0x86, 0xc7, 0x7d, 0xdc, 0x34, 0x79, 0x45, 0x4f, 0x23, 0x2f, 0xdf, 0x3e, - 0x7e, 0x34, 0x0f, 0x79, 0x6f, 0xe1, 0x97, 0x07, 0x64, 0x40, 0x1a, 0x26, 0x71, 0x3b, 0x39, 0xbf, - 0x27, 0xd0, 0x87, 0x02, 0x24, 0x55, 0x32, 0x94, 0x15, 0x55, 0xb5, 0x78, 0x31, 0xff, 0xb5, 0xf0, - 0x6e, 0x69, 0x69, 0x0b, 0xf2, 0xeb, 0x5f, 0xbb, 0x76, 0xad, 0x54, 0xae, 0x6c, 0xe4, 0xdf, 0x8b, - 0x08, 0x89, 0x5f, 0x46, 0xe2, 0x4e, 0xb5, 0xd1, 0xbb, 0x27, 0xe5, 0xf8, 0x9b, 0xb1, 0x83, 0x98, - 0x29, 0x7c, 0xf4, 0x70, 0xe9, 0x6d, 0x01, 0x6e, 0x75, 0x8d, 0x02, 0x3d, 0x20, 0x94, 0xd5, 0xa4, - 0x02, 0x6f, 0x9d, 0x8a, 0xc1, 0xa7, 0xb2, 0xe1, 0xf5, 0xa2, 0x79, 0xd8, 0x2d, 0xd2, 0x91, 0x49, - 0xec, 0xc2, 0xae, 0x62, 0xd9, 0x07, 0x4a, 0x6f, 0xb3, 0x7a, 0x9f, 0xd5, 0x24, 0x74, 0x6e, 0x80, - 0xbb, 0x7a, 0xdf, 0x85, 0xf8, 0xaa, 0x5b, 0xd4, 0x12, 0x2a, 0x19, 0x96, 0x54, 0xd5, 0x9a, 0xe1, - 0xab, 0xc8, 0xa9, 0x44, 0xff, 0x34, 0xe4, 0xfa, 0x9a, 0xee, 0x8f, 0x80, 0x5b, 0xe2, 0xd3, 0x7d, - 0x4d, 0x1f, 0x07, 0xe0, 0xcf, 0x02, 0x88, 0x1e, 0xf7, 0x8e, 0xfd, 0x14, 0xfb, 0x5f, 0xf4, 0x93, - 0xd7, 0x2b, 0x6c, 0x70, 0x77, 0xbd, 0x04, 0x17, 0x43, 0x4d, 0x85, 0xe7, 0xb6, 0xf9, 0x90, 0xdb, - 0x1e, 0x0b, 0x76, 0x19, 0xae, 0xf7, 0xd6, 0x27, 0xea, 0x21, 0x2f, 0xc6, 0x99, 0x17, 0x11, 0x9f, - 0xdd, 0x9d, 0x38, 0x33, 0xaf, 0xc1, 0x45, 0xdf, 0xb1, 0x68, 0xb9, 0x0d, 0xed, 0x86, 0x73, 0x23, - 0xfb, 0xf4, 0x76, 0xf7, 0x79, 0x88, 0xb1, 0x1b, 0x5e, 0xe4, 0x74, 0x72, 0x62, 0x42, 0xbe, 0xf4, - 0xff, 0x6d, 0x12, 0xb2, 0x81, 0xcb, 0x07, 0x6a, 0x4f, 0x3d, 0x8f, 0x09, 0x67, 0x7f, 0x1e, 0xf3, - 0x55, 0x8c, 0xf0, 0x43, 0xd9, 0x14, 0x33, 0x44, 0xce, 0xf0, 0x50, 0x14, 0x6a, 0xd4, 0x32, 0xe7, - 0xba, 0x3f, 0x6e, 0x41, 0x6e, 0x60, 0xce, 0x78, 0x18, 0xfa, 0xff, 0x47, 0xde, 0xbe, 0x36, 0xe7, - 0x70, 0x76, 0x10, 0x78, 0xbf, 0xd8, 0x84, 0xf4, 0xeb, 0x86, 0xa6, 0xcb, 0x0a, 0x6b, 0x96, 0xf9, - 0x63, 0xdf, 0x33, 0xa7, 0x00, 0x4d, 0x3a, 0xeb, 0xcd, 0x39, 0x0c, 0xaf, 0x4f, 0xfa, 0xec, 0x4d, - 0xc8, 0x8c, 0x1f, 0xac, 0x94, 0xce, 0x21, 0xef, 0x26, 0xce, 0x52, 0x17, 0x37, 0xe7, 0x70, 0xda, - 0x53, 0x2d, 0x75, 0x0e, 0xd1, 0x16, 0x64, 0xc7, 0x48, 0xba, 0x03, 0x15, 0x3f, 0x0f, 0xd4, 0xd8, - 0x8a, 0xba, 0x12, 0xc2, 0xb2, 0x89, 0x4e, 0x79, 0x43, 0x71, 0x5e, 0xac, 0x16, 0xd1, 0x29, 0x6a, - 0xc3, 0xf8, 0x65, 0x4d, 0xde, 0x67, 0x04, 0xc6, 0xd9, 0xf7, 0xb9, 0x33, 0xa0, 0xb9, 0x8c, 0xb7, - 0x39, 0x87, 0x73, 0x6a, 0x90, 0x03, 0xeb, 0x3e, 0xd4, 0x37, 0x9c, 0x42, 0xad, 0xf2, 0x37, 0x81, - 0x33, 0xda, 0x38, 0xc6, 0x63, 0x55, 0x5e, 0x45, 0x06, 0x2c, 0x07, 0xf1, 0x64, 0xdf, 0x9d, 0x82, - 0x3f, 0x65, 0x17, 0x4f, 0x81, 0x9e, 0xc5, 0x78, 0x9b, 0x73, 0x58, 0x0a, 0x2c, 0xe3, 0x13, 0x72, - 0x36, 0xe0, 0x5d, 0x2e, 0x65, 0xdb, 0xe8, 0x0d, 0xf9, 0x8b, 0xc3, 0xe9, 0x1b, 0xf0, 0x2e, 0x95, - 0xce, 0x06, 0x3c, 0xed, 0x16, 0x53, 0x46, 0xdb, 0x90, 0xe1, 0x87, 0x5f, 0x66, 0x27, 0xdf, 0x7d, - 0x99, 0xf8, 0xd2, 0x29, 0x60, 0xbe, 0x4a, 0xe2, 0xe4, 0x92, 0xed, 0x2b, 0x2c, 0x97, 0x20, 0x65, - 0x6b, 0xfd, 0x41, 0x8f, 0x6d, 0x3e, 0xe7, 0xb6, 0x9a, 0xe3, 0x81, 0x49, 0xbd, 0x28, 0xa7, 0x20, - 0x32, 0x30, 0xdd, 0x07, 0xe1, 0x3f, 0x46, 0x40, 0xe2, 0xc7, 0x83, 0x37, 0xae, 0xb7, 0x0d, 0xab, - 0xaf, 0x50, 0x4a, 0x2c, 0x1b, 0xed, 0x42, 0x66, 0x60, 0xca, 0xfb, 0xde, 0x00, 0xab, 0x21, 0xb9, - 0xe9, 0x67, 0xc4, 0xb0, 0xa2, 0xaf, 0xab, 0x4c, 0x0f, 0xcc, 0xf1, 0x30, 0xba, 0x05, 0x17, 0xfd, - 0x70, 0xb2, 0xa9, 0x58, 0x4a, 0x9f, 0x38, 0xc0, 0xec, 0xae, 0x5b, 0x4e, 0x9d, 0x94, 0xe3, 0x56, - 0x4c, 0x7a, 0xeb, 0x83, 0x08, 0x5e, 0xf4, 0xe9, 0x35, 0x3d, 0x31, 0xf4, 0x32, 0xb0, 0xf8, 0xfb, - 0x2c, 0x8a, 0x9e, 0xdb, 0x22, 0x76, 0x42, 0x26, 0x36, 0x55, 0x40, 0x0a, 0x42, 0xfa, 0xac, 0x8a, - 0x85, 0xad, 0xba, 0x18, 0xd0, 0x1d, 0xdb, 0xe5, 0x6b, 0x2e, 0x7f, 0x27, 0xc0, 0x62, 0xa0, 0x11, - 0xe1, 0xff, 0x37, 0xf8, 0x82, 0x0a, 0xf2, 0xae, 0xbf, 0x37, 0x8b, 0x9c, 0xb9, 0x37, 0x2b, 0xc3, - 0x49, 0x39, 0xf1, 0xae, 0x10, 0x13, 0x7f, 0xf1, 0xc3, 0xb8, 0xaf, 0x4f, 0xbb, 0xfa, 0x2b, 0x01, - 0xc4, 0xb0, 0xeb, 0x10, 0x82, 0xdc, 0xed, 0x06, 0xde, 0x2d, 0xb5, 0xdb, 0x55, 0x2c, 0xd7, 0x1b, - 0xf5, 0xaa, 0x38, 0x87, 0x24, 0x58, 0x9c, 0x8c, 0xe1, 0x6a, 0xb3, 0xd1, 0xaa, 0xb5, 0x1b, 0xf8, - 0x81, 0x28, 0xa0, 0x65, 0xb8, 0x38, 0x99, 0xb9, 0x83, 0x9b, 0x15, 0xb9, 0x55, 0xc5, 0xf7, 0x6a, - 0x95, 0xaa, 0x18, 0x09, 0x6a, 0x6d, 0x95, 0xee, 0x95, 0x5a, 0x15, 0x5c, 0x6b, 0xb6, 0xc5, 0x68, - 0x70, 0xa6, 0x52, 0x7a, 0x50, 0xad, 0xd7, 0xab, 0x3b, 0xcd, 0xa6, 0x18, 0x5b, 0xbe, 0xf0, 0xd1, - 0xc3, 0xa5, 0xac, 0x24, 0x5c, 0x4d, 0x8d, 0xe7, 0xcb, 0x37, 0x3e, 0xf8, 0xdb, 0x53, 0xc2, 0x77, - 0x8a, 0xe7, 0x68, 0x11, 0xa8, 0x6e, 0xee, 0xed, 0xc5, 0x19, 0xd1, 0x5c, 0xff, 0x57, 0x00, 0x00, - 0x00, 0xff, 0xff, 0x72, 0x48, 0xaa, 0x27, 0xec, 0x1e, 0x00, 0x00, + // 2772 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x59, 0x4f, 0x6c, 0xe3, 0xc6, + 0xd5, 0x37, 0x25, 0x59, 0x7f, 0x9e, 0xfe, 0x98, 0x9e, 0x38, 0x1b, 0xda, 0xd9, 0x64, 0xfd, 0x29, + 0x9b, 0x2f, 0xce, 0xe6, 0xb3, 0xbc, 0x9f, 0xb7, 0xdb, 0xa4, 0xdb, 0x26, 0x5b, 0x49, 0x96, 0xd7, + 0xf2, 0x1f, 0x49, 0x19, 0x69, 0x37, 0xbb, 0x09, 0x52, 0x82, 0x16, 0xc7, 0x32, 0x63, 0x8a, 0x64, + 0x48, 0x4a, 0xb6, 0xb7, 0x28, 0x10, 0xe4, 0xd6, 0x00, 0x3d, 0x34, 0x28, 0x50, 0x34, 0x2d, 0x8a, + 0xa2, 0x05, 0x72, 0x58, 0x14, 0x3d, 0x14, 0xbd, 0xf5, 0x92, 0x4b, 0x81, 0x1c, 0xda, 0x5b, 0x81, + 0x5e, 0x0b, 0xb4, 0x97, 0x06, 0xe8, 0x25, 0xa7, 0xc2, 0x97, 0x14, 0x1c, 0x0e, 0x25, 0x92, 0xd2, + 0xda, 0xd6, 0x26, 0x29, 0xd2, 0x8b, 0x61, 0xce, 0xbc, 0xf7, 0x9b, 0x37, 0x6f, 0xde, 0xfb, 0xbd, + 0x37, 0x23, 0x58, 0x54, 0x75, 0x53, 0x3a, 0x94, 0xb4, 0x65, 0xcb, 0x96, 0xda, 0x07, 0x2b, 0x92, + 0xa1, 0xac, 0x74, 0x89, 0x65, 0x49, 0x1d, 0x62, 0x15, 0x0c, 0x53, 0xb7, 0x75, 0x94, 0xb3, 0x6d, + 0xad, 0xc0, 0xa4, 0x0a, 0xfd, 0x6b, 0x0b, 0xc5, 0x8e, 0x62, 0xef, 0xf7, 0x76, 0x0b, 0x6d, 0xbd, + 0xbb, 0x42, 0xb4, 0xbe, 0x7e, 0x6c, 0x98, 0xfa, 0xd1, 0xf1, 0x0a, 0x15, 0x6e, 0x2f, 0x77, 0x88, + 0xb6, 0xdc, 0x97, 0x54, 0x45, 0x96, 0x6c, 0xb2, 0x32, 0xf2, 0x8f, 0x0b, 0xb9, 0xb0, 0xec, 0x83, + 0xe8, 0xe8, 0x1d, 0xdd, 0x55, 0xde, 0xed, 0xed, 0xd1, 0x2f, 0xfa, 0x41, 0xff, 0x63, 0xe2, 0x65, + 0x9f, 0x78, 0x6b, 0x9f, 0xb4, 0xf6, 0x15, 0xad, 0x63, 0x55, 0x35, 0xb9, 0x67, 0xd9, 0xa6, 0x42, + 0x2c, 0xff, 0xd2, 0x1d, 0x7d, 0xf9, 0x2d, 0x4b, 0xd7, 0x56, 0x24, 0x4d, 0xd3, 0x6d, 0xc9, 0x56, + 0x74, 0x8d, 0x6d, 0x63, 0x61, 0x6d, 0x22, 0x90, 0x3d, 0x55, 0xea, 0x58, 0x63, 0x50, 0x2e, 0x76, + 0x74, 0xbd, 0xa3, 0x92, 0xa1, 0xc1, 0x96, 0x6d, 0xf6, 0xda, 0x36, 0x9b, 0xbd, 0x14, 0x9e, 0xb5, + 0x95, 0x2e, 0xb1, 0x6c, 0xa9, 0x6b, 0x30, 0x81, 0xa7, 0xc3, 0x02, 0x72, 0xcf, 0xa4, 0xf8, 0x6c, + 0xfe, 0xa9, 0xd1, 0xd3, 0x20, 0xa6, 0xa9, 0x9b, 0x6c, 0xfa, 0x99, 0xd1, 0x69, 0x45, 0x26, 0x9a, + 0xad, 0xec, 0x29, 0xc4, 0x1c, 0x98, 0x38, 0x2a, 0x74, 0x40, 0x8e, 0xbd, 0xd9, 0x4b, 0xa3, 0xb3, + 0xde, 0xd9, 0xba, 0x02, 0x63, 0x03, 0xc2, 0x96, 0x64, 0xc9, 0x96, 0x98, 0x44, 0xc1, 0xe7, 0x2a, + 0xdd, 0x20, 0x9a, 0x64, 0x28, 0xfd, 0xd5, 0x15, 0xdd, 0xa0, 0x7e, 0x1a, 0xf5, 0x59, 0xfe, 0xef, + 0x51, 0xc8, 0xde, 0x36, 0x54, 0x45, 0x3b, 0xd8, 0x71, 0x23, 0x0b, 0x5d, 0x82, 0xb4, 0x29, 0x1d, + 0x8a, 0x86, 0x74, 0xac, 0xea, 0x92, 0x2c, 0x70, 0x8b, 0xdc, 0x52, 0x06, 0x83, 0x29, 0x1d, 0x36, + 0xdc, 0x11, 0xf4, 0xff, 0x90, 0xf0, 0x26, 0x23, 0x8b, 0xdc, 0x52, 0x7a, 0xf5, 0x89, 0x42, 0x30, + 0x0a, 0x0b, 0x0c, 0x0a, 0x7b, 0x72, 0xe8, 0xdb, 0x90, 0xb4, 0x88, 0x6d, 0x3b, 0xa7, 0x2a, 0xc4, + 0xa8, 0xce, 0x42, 0x58, 0xa7, 0x75, 0xd4, 0x64, 0x12, 0xa5, 0xe4, 0x49, 0x69, 0xfa, 0x3d, 0x2e, + 0xc2, 0x73, 0x78, 0xa0, 0x85, 0xbe, 0x09, 0x69, 0xf3, 0x48, 0xf4, 0x36, 0x2b, 0x4c, 0x2f, 0x46, + 0xc7, 0x81, 0xe0, 0xa3, 0x1d, 0x26, 0x81, 0xc1, 0x1c, 0xfc, 0x4f, 0x95, 0x49, 0x9b, 0x28, 0x7d, + 0x22, 0x8b, 0x92, 0x2d, 0xc4, 0x99, 0x05, 0xee, 0x79, 0x17, 0xbc, 0xf3, 0x2e, 0xb4, 0xbc, 0x80, + 0xc0, 0xe0, 0x89, 0x17, 0x6d, 0x74, 0x1d, 0x66, 0xda, 0xba, 0x69, 0x12, 0x95, 0xfa, 0x4d, 0x54, + 0x64, 0x4b, 0x48, 0x2c, 0x46, 0x97, 0x52, 0xa5, 0xcc, 0x49, 0x29, 0xf5, 0x3e, 0x17, 0xcf, 0xc7, + 0xcc, 0x88, 0x20, 0xe3, 0x9c, 0x4f, 0xa8, 0x2a, 0x5b, 0xe8, 0x06, 0xcc, 0xc9, 0xa4, 0xaf, 0xb4, + 0x89, 0xd8, 0xde, 0x97, 0x34, 0x8d, 0xa8, 0xa2, 0xa2, 0xc9, 0xe4, 0x48, 0x48, 0x2d, 0x72, 0x4b, + 0x59, 0xba, 0xc5, 0x2b, 0x51, 0xe1, 0x33, 0x0e, 0x23, 0x57, 0xaa, 0xec, 0x0a, 0x55, 0x1d, 0x19, + 0xb4, 0x06, 0x7c, 0x5b, 0xd7, 0xac, 0x5e, 0xd7, 0xb1, 0x57, 0x31, 0x9d, 0x40, 0x15, 0x80, 0x1a, + 0x3d, 0x3f, 0x62, 0xf4, 0x1a, 0x0b, 0x52, 0x3c, 0xe3, 0xa9, 0x14, 0x5d, 0x8d, 0xcd, 0x58, 0x32, + 0xca, 0xc7, 0x36, 0x63, 0xc9, 0x24, 0x9f, 0xca, 0xff, 0x2c, 0x0a, 0x33, 0x6b, 0xfa, 0xa1, 0xf6, + 0x65, 0x1f, 0xf4, 0x26, 0xe4, 0x88, 0x26, 0x8b, 0x6c, 0xe7, 0x8e, 0xaf, 0xa2, 0x54, 0xf3, 0x72, + 0x58, 0xb3, 0xa2, 0xc9, 0x6b, 0x54, 0xa8, 0x3a, 0xcc, 0x11, 0x9c, 0x21, 0xc3, 0x51, 0x0b, 0x5d, + 0x87, 0x84, 0x49, 0xde, 0xee, 0x11, 0xcb, 0x66, 0x31, 0x33, 0x3f, 0x1a, 0x33, 0xd8, 0x15, 0xd8, + 0x98, 0xc2, 0x9e, 0x2c, 0xba, 0x01, 0x29, 0xab, 0xbd, 0x4f, 0xe4, 0x9e, 0x4a, 0x64, 0x61, 0xfa, + 0xac, 0x60, 0xdb, 0x98, 0xc2, 0x43, 0xf1, 0x71, 0x67, 0x1d, 0x3f, 0xc7, 0x59, 0x17, 0x20, 0x67, + 0x11, 0xcb, 0x72, 0x54, 0x0e, 0xc8, 0xb1, 0xa8, 0xc8, 0x42, 0xc2, 0x71, 0x26, 0x3d, 0xe5, 0xfb, + 0x51, 0xe1, 0x1d, 0x1e, 0x67, 0xd8, 0xfc, 0x16, 0x39, 0xae, 0xca, 0xa5, 0x99, 0x61, 0x3a, 0xa0, + 0xe8, 0xbf, 0x4a, 0x5c, 0xfe, 0xfb, 0x51, 0xe0, 0x5b, 0x47, 0xc5, 0xf6, 0x81, 0xa6, 0x1f, 0xaa, + 0x44, 0xee, 0x74, 0x89, 0x36, 0x36, 0xf0, 0xb8, 0x73, 0x18, 0x53, 0x85, 0xb8, 0x49, 0xac, 0x9e, + 0x6a, 0xd3, 0x43, 0xcb, 0xad, 0x3e, 0x37, 0xba, 0xf9, 0xe0, 0x42, 0x05, 0x4c, 0xc5, 0xa9, 0xb5, + 0xef, 0xd2, 0xb4, 0x63, 0x00, 0x68, 0x13, 0x78, 0x99, 0x05, 0x8d, 0xc8, 0x0a, 0x0f, 0x3b, 0xcf, + 0x4b, 0x61, 0xd0, 0x50, 0x70, 0xe1, 0x19, 0x39, 0x38, 0x90, 0xff, 0x90, 0x83, 0xb8, 0xbb, 0x10, + 0x4a, 0x43, 0xa2, 0x79, 0xbb, 0x5c, 0xae, 0x34, 0x9b, 0xfc, 0x14, 0x9a, 0x85, 0xec, 0xed, 0xda, + 0x56, 0xad, 0xfe, 0x5a, 0x4d, 0xac, 0x60, 0x5c, 0xc7, 0x3c, 0x87, 0x32, 0x90, 0x6c, 0xd5, 0xeb, + 0xe2, 0x76, 0xb1, 0x55, 0xe1, 0x23, 0x28, 0x0b, 0x29, 0xe7, 0xab, 0x52, 0xc4, 0xdb, 0xf7, 0xf8, + 0x28, 0x9a, 0x03, 0xbe, 0x5c, 0xdf, 0xde, 0xae, 0x36, 0xab, 0xf5, 0x9a, 0xd8, 0x28, 0x96, 0xb7, + 0x2a, 0x2d, 0x3e, 0x16, 0x1c, 0x2d, 0x55, 0x8a, 0xe5, 0x7a, 0x8d, 0x9f, 0x76, 0x16, 0x6a, 0xdd, + 0x15, 0xd7, 0x71, 0xe5, 0x55, 0x3e, 0x4e, 0x51, 0xef, 0x8a, 0x8d, 0xfa, 0x6b, 0x15, 0xcc, 0x27, + 0x10, 0x0f, 0x99, 0x5b, 0x8d, 0xa6, 0x78, 0xbb, 0xb6, 0x5d, 0x2f, 0x6f, 0x55, 0xd6, 0xf8, 0xe4, + 0x42, 0xfc, 0x93, 0x07, 0xf3, 0x11, 0x81, 0xcb, 0xff, 0x98, 0x83, 0x27, 0x6e, 0x49, 0x36, 0x39, + 0x94, 0x8e, 0x47, 0x8e, 0xa4, 0x0c, 0xe9, 0x8e, 0x3b, 0xc5, 0x8e, 0xc3, 0xf1, 0x45, 0x3e, 0xec, + 0x0b, 0xa6, 0xed, 0x8f, 0x6c, 0xe8, 0x78, 0x63, 0x16, 0x7a, 0x11, 0xe2, 0xf6, 0x91, 0x28, 0xb5, + 0x0f, 0x58, 0x56, 0x2d, 0x9e, 0x75, 0x40, 0x78, 0xda, 0x76, 0x46, 0xf2, 0x26, 0xcc, 0x31, 0xe8, + 0x20, 0x63, 0x17, 0x21, 0xe1, 0x9d, 0x8e, 0x6b, 0xd1, 0x53, 0x61, 0xc4, 0x80, 0xbc, 0x8f, 0x5f, + 0x3d, 0x3d, 0xf4, 0x04, 0x24, 0x76, 0x25, 0x4d, 0x76, 0x42, 0xd7, 0x31, 0x2a, 0x85, 0xe3, 0xce, + 0x67, 0x55, 0xce, 0xff, 0x23, 0x09, 0xb3, 0x45, 0xc3, 0x50, 0x95, 0x36, 0x0d, 0x30, 0x17, 0x68, + 0x4c, 0xc0, 0x73, 0xa7, 0x05, 0x3c, 0xca, 0x43, 0x7c, 0x4f, 0x34, 0x74, 0xd3, 0x8d, 0xc9, 0x6c, + 0x29, 0x7d, 0x52, 0x4a, 0x5e, 0x89, 0x0b, 0x9f, 0x71, 0x2f, 0xfd, 0x95, 0xc3, 0xd3, 0x7b, 0x0d, + 0xdd, 0xb4, 0xd1, 0x63, 0x30, 0xbd, 0x27, 0xb6, 0x35, 0x9b, 0x46, 0x58, 0x16, 0xc7, 0xf6, 0xca, + 0x9a, 0xed, 0x70, 0xd4, 0x9e, 0xd9, 0x1d, 0x70, 0x54, 0xcc, 0xe5, 0xa8, 0x3d, 0xb3, 0xdb, 0x18, + 0x54, 0x96, 0x19, 0x99, 0xb4, 0x75, 0x99, 0xc8, 0x03, 0xa1, 0x69, 0xc6, 0x55, 0x61, 0xa6, 0x6c, + 0xd2, 0x6e, 0x00, 0xe7, 0x98, 0xbc, 0x87, 0xf0, 0x12, 0x08, 0x21, 0x04, 0xf1, 0x50, 0x32, 0x35, + 0x5a, 0xab, 0x32, 0x4e, 0xbe, 0xe1, 0x0b, 0x41, 0x8d, 0xd7, 0xd8, 0x2c, 0x5a, 0x07, 0xa4, 0xe9, + 0x66, 0x57, 0x52, 0x95, 0xfb, 0xbe, 0xe5, 0x67, 0x69, 0x69, 0x7a, 0xe8, 0xf2, 0xb3, 0x43, 0x15, + 0xcf, 0x82, 0x57, 0xe0, 0xc9, 0x51, 0x9c, 0xa1, 0x11, 0x88, 0x1a, 0x31, 0x3f, 0xa2, 0x37, 0xb0, + 0x23, 0x54, 0x1b, 0xe3, 0x13, 0xd5, 0x46, 0x7f, 0x69, 0x4e, 0x3c, 0x72, 0x69, 0xf6, 0x55, 0xd7, + 0xe4, 0x44, 0xd5, 0xf5, 0x45, 0x48, 0x49, 0x86, 0x21, 0x5a, 0x4e, 0x1c, 0xd1, 0xda, 0x98, 0x5e, + 0x7d, 0x32, 0xbc, 0xfe, 0x16, 0x39, 0xae, 0x68, 0x7d, 0xa2, 0xea, 0x06, 0xc1, 0x09, 0xc9, 0x30, + 0x9a, 0x5b, 0xe4, 0x18, 0x2d, 0xc1, 0xac, 0x2a, 0x59, 0xb6, 0x28, 0x89, 0x34, 0x6a, 0x44, 0x87, + 0x6f, 0x68, 0x91, 0xcc, 0xe2, 0xac, 0x33, 0x51, 0x5c, 0x2f, 0x6b, 0xb6, 0xc3, 0x4a, 0xe8, 0x22, + 0xa4, 0xda, 0xba, 0xb6, 0xa7, 0x98, 0x5d, 0x22, 0x0b, 0xe9, 0x45, 0x6e, 0x29, 0x89, 0x87, 0x03, + 0x63, 0x6b, 0x6d, 0x76, 0xd2, 0x5a, 0x8b, 0x6a, 0x90, 0x52, 0x75, 0x37, 0x45, 0x2c, 0x21, 0x47, + 0x0f, 0xe0, 0x6a, 0x78, 0x1b, 0x23, 0x69, 0x54, 0xd8, 0xf6, 0x54, 0x2a, 0x9a, 0x6d, 0x1e, 0xe3, + 0x21, 0x04, 0xda, 0x86, 0x74, 0x9f, 0x98, 0x96, 0xc7, 0xfb, 0x33, 0xd4, 0xa0, 0x17, 0x1e, 0x5a, + 0x44, 0xef, 0xb8, 0xb2, 0x01, 0xc6, 0xe9, 0x7b, 0x63, 0x96, 0x43, 0x5b, 0x1a, 0xb1, 0x0f, 0x75, + 0xf3, 0x80, 0xa2, 0xf1, 0xe3, 0x69, 0xab, 0xe6, 0x8a, 0x04, 0x40, 0x34, 0x6f, 0xcc, 0x5a, 0xb8, + 0x03, 0xb9, 0xa0, 0xbd, 0x88, 0x87, 0xa8, 0x73, 0x6a, 0x1c, 0x25, 0x0c, 0xe7, 0x5f, 0x54, 0x80, + 0xe9, 0xbe, 0xa4, 0xf6, 0x08, 0x63, 0x36, 0x21, 0xbc, 0x84, 0x07, 0x80, 0x5d, 0xb1, 0x1b, 0x91, + 0x97, 0xb8, 0x1b, 0xc9, 0x4f, 0x1f, 0xcc, 0xc7, 0x92, 0x1c, 0x3f, 0x95, 0xff, 0x5d, 0x02, 0x9e, + 0x1c, 0x71, 0x52, 0x6d, 0x10, 0xf6, 0x13, 0xb3, 0xce, 0xe5, 0x10, 0xeb, 0x64, 0x4f, 0x4a, 0x70, + 0x25, 0x29, 0x7c, 0xc6, 0x2d, 0x7d, 0x7e, 0xde, 0x69, 0x8c, 0xcd, 0xfd, 0xd3, 0xa9, 0xc7, 0x97, + 0x3d, 0x93, 0xb3, 0x40, 0x7c, 0x42, 0x16, 0x48, 0x3c, 0x32, 0x0b, 0x24, 0x1f, 0x89, 0x05, 0x2a, + 0x41, 0x16, 0x48, 0x9d, 0xc5, 0x02, 0x14, 0xe4, 0xb7, 0x5c, 0x24, 0xc9, 0x05, 0xf8, 0x20, 0x90, + 0xac, 0x70, 0x9e, 0x64, 0x4d, 0x4f, 0x9c, 0xac, 0x77, 0xfd, 0xc9, 0x9a, 0xa1, 0x7e, 0xba, 0x71, + 0x66, 0xb2, 0x0e, 0xe3, 0xf0, 0xfc, 0x69, 0x9b, 0xfd, 0x42, 0xd3, 0x36, 0xf7, 0x15, 0x4d, 0xdb, + 0x3f, 0x44, 0xe0, 0x31, 0x9f, 0xbb, 0x3c, 0x61, 0x24, 0x40, 0xc2, 0x22, 0xa6, 0xb3, 0x4f, 0xb6, + 0x96, 0xf7, 0x89, 0x5e, 0x81, 0xa4, 0xe7, 0xb3, 0xb3, 0x96, 0xf4, 0xc7, 0x9a, 0xa7, 0x83, 0xde, + 0xe3, 0x00, 0x24, 0xdb, 0x36, 0x95, 0xdd, 0x9e, 0x4d, 0x9c, 0x2b, 0x86, 0x73, 0x84, 0xd7, 0x4e, + 0x39, 0x42, 0x0f, 0xad, 0x50, 0x1c, 0x68, 0x51, 0x5f, 0x94, 0xae, 0x9f, 0x94, 0x56, 0x3f, 0xe0, + 0x56, 0x78, 0xc8, 0x5f, 0x36, 0xf3, 0xc2, 0xe5, 0xd5, 0xa7, 0xbf, 0xf3, 0x86, 0xb4, 0x7c, 0xff, + 0xea, 0xf2, 0x37, 0xde, 0x5c, 0xba, 0x79, 0xe3, 0x8d, 0xe5, 0x37, 0x6f, 0x7a, 0x9f, 0xcf, 0x7f, + 0x77, 0xf5, 0xff, 0xbe, 0x77, 0xf9, 0xca, 0xb4, 0x19, 0x15, 0x3e, 0xe6, 0xb0, 0x6f, 0xf5, 0x85, + 0x97, 0x61, 0x26, 0x84, 0x3a, 0xc6, 0xc3, 0x73, 0x7e, 0x0f, 0xa7, 0xc6, 0xfb, 0xf1, 0xcf, 0x11, + 0x78, 0xdc, 0x67, 0xf3, 0xa6, 0xae, 0x68, 0xc5, 0x76, 0x9b, 0x18, 0xf6, 0xc4, 0xc4, 0x17, 0x28, + 0xaa, 0x91, 0x09, 0x8a, 0xea, 0x5d, 0x78, 0x5c, 0xd1, 0xbc, 0xf7, 0x20, 0x59, 0xf4, 0x7a, 0x78, + 0xcf, 0xc5, 0xcf, 0x9c, 0xe2, 0x62, 0xef, 0x02, 0x80, 0xe7, 0x7c, 0x08, 0xde, 0xa0, 0x85, 0x9e, + 0x83, 0x19, 0x83, 0x68, 0xb2, 0xa2, 0x75, 0x44, 0x66, 0x2a, 0x25, 0xd5, 0x24, 0xce, 0xb1, 0xe1, + 0xa6, 0x3b, 0x1a, 0xe6, 0x91, 0xe4, 0xa3, 0xf1, 0x88, 0xcf, 0xad, 0xff, 0x4c, 0x04, 0xc2, 0xd3, + 0x33, 0x09, 0xfd, 0x89, 0x7b, 0x88, 0x57, 0x3f, 0xe4, 0x3c, 0xb7, 0x7e, 0xfa, 0x60, 0xfe, 0x27, + 0xdc, 0x42, 0xed, 0x11, 0x1e, 0xa7, 0xe8, 0x5f, 0x43, 0xed, 0x75, 0x14, 0xad, 0x50, 0x23, 0x87, + 0x1b, 0xe4, 0xa8, 0x74, 0x6c, 0x13, 0x6b, 0x5d, 0x95, 0x3a, 0xf9, 0x5b, 0x9f, 0x13, 0xef, 0x16, + 0xb1, 0x29, 0xd8, 0x17, 0xd5, 0x63, 0x7f, 0xc4, 0x8d, 0x29, 0x76, 0xa5, 0x9f, 0x73, 0x5f, 0xf1, + 0xdd, 0xff, 0xe7, 0x6e, 0x01, 0x70, 0xea, 0x2d, 0x20, 0x50, 0xb1, 0xe2, 0xe1, 0x8a, 0x75, 0x0b, + 0x52, 0x6d, 0x55, 0xb2, 0x2c, 0x71, 0x57, 0x6c, 0xb3, 0xfe, 0xfa, 0x85, 0x73, 0x64, 0x51, 0xa1, + 0xec, 0x28, 0x95, 0xca, 0x38, 0xd1, 0x76, 0xff, 0x41, 0x1b, 0x90, 0x34, 0x4c, 0x45, 0x37, 0x15, + 0xfb, 0x98, 0x26, 0x45, 0x6e, 0xb4, 0x12, 0xb4, 0x8e, 0x9a, 0xec, 0x25, 0xa3, 0xc1, 0x24, 0x7d, + 0x77, 0xfa, 0x81, 0xf6, 0xb8, 0x77, 0x85, 0xd4, 0xd9, 0xef, 0x0a, 0x0b, 0x3f, 0xe5, 0x20, 0xc1, + 0xac, 0x42, 0x15, 0x48, 0xb2, 0x0b, 0xad, 0xc5, 0x1a, 0x8d, 0xe7, 0xc3, 0xc6, 0x30, 0xd1, 0x31, + 0x77, 0xe1, 0x81, 0x2a, 0xba, 0x09, 0x59, 0x69, 0xd7, 0xd2, 0xd5, 0x9e, 0x4d, 0x44, 0x5a, 0xcb, + 0xcf, 0xbe, 0x3b, 0x64, 0x3c, 0x05, 0x67, 0x68, 0x90, 0xe5, 0x43, 0x1a, 0xe5, 0xf2, 0xf7, 0x60, + 0x6e, 0x8c, 0x43, 0x2d, 0x54, 0x84, 0xd4, 0x90, 0xcf, 0xb8, 0xf3, 0xf3, 0xd9, 0x50, 0x2b, 0xff, + 0x1b, 0x0e, 0xe6, 0xc7, 0x88, 0xac, 0x4b, 0x8a, 0x4a, 0x64, 0x54, 0x85, 0xa4, 0x27, 0xca, 0xee, + 0xe1, 0xe7, 0xc1, 0xf7, 0x17, 0x38, 0x4f, 0x1d, 0x7d, 0x0b, 0xa6, 0xe9, 0xd3, 0x32, 0x23, 0xef, + 0x8b, 0x23, 0x1d, 0x84, 0x33, 0xb9, 0x46, 0x6c, 0x49, 0x51, 0xfd, 0xdd, 0x98, 0xab, 0xe4, 0x2f, + 0xcd, 0x1c, 0x5c, 0xf2, 0xad, 0x59, 0x1d, 0xc7, 0xcc, 0x9f, 0xdf, 0x2f, 0xe8, 0x59, 0x98, 0xa1, + 0x77, 0x31, 0xdf, 0x4d, 0x8c, 0x72, 0x10, 0xce, 0x38, 0xc3, 0x83, 0x8b, 0xd8, 0x68, 0x19, 0x8b, + 0x9e, 0x56, 0xc6, 0x7c, 0xfb, 0xf8, 0xe1, 0x34, 0xe4, 0xbd, 0x85, 0x5f, 0xed, 0x91, 0x1e, 0xa9, + 0x1b, 0xc4, 0xed, 0xee, 0xfc, 0x9e, 0x40, 0x1f, 0x73, 0x90, 0x94, 0x49, 0x5f, 0x94, 0x64, 0xd9, + 0x64, 0x64, 0xfe, 0x6b, 0xee, 0xfd, 0xe2, 0xfc, 0x26, 0xe4, 0x57, 0xbf, 0x7e, 0xf5, 0x6a, 0xb1, + 0x54, 0x5e, 0xcb, 0x7f, 0x10, 0xe1, 0x12, 0xbf, 0x8c, 0xc4, 0x1d, 0xb6, 0xd1, 0x3a, 0x27, 0xa5, + 0xf8, 0xfd, 0xd8, 0x7e, 0xcc, 0xe0, 0x3e, 0x79, 0x30, 0xff, 0x2e, 0x07, 0x37, 0x3b, 0x7a, 0xc1, + 0xde, 0x27, 0x36, 0xe5, 0xa4, 0x02, 0x6b, 0xa2, 0x56, 0x82, 0x6f, 0xee, 0xfd, 0x6b, 0x2b, 0xc6, + 0x41, 0x67, 0xc5, 0x3e, 0x36, 0x88, 0x55, 0xd8, 0x91, 0x4c, 0x6b, 0x5f, 0x52, 0x37, 0x2a, 0x77, + 0x29, 0x27, 0xa1, 0x89, 0x01, 0x6e, 0x6b, 0x5d, 0x17, 0xe2, 0x6b, 0x2e, 0xa9, 0x25, 0x64, 0xd2, + 0x2f, 0xca, 0xb2, 0x39, 0xc6, 0x57, 0x91, 0x53, 0x4b, 0xfe, 0x33, 0x90, 0xeb, 0x2a, 0x9a, 0xff, + 0x04, 0x5c, 0x8a, 0x4f, 0x77, 0x15, 0x6d, 0x70, 0x00, 0x7f, 0xe1, 0x80, 0xf7, 0xaa, 0xf0, 0xc0, + 0x4f, 0xb1, 0xff, 0x46, 0x3f, 0x79, 0x5d, 0xc3, 0x1a, 0x73, 0xd7, 0xcb, 0x70, 0x21, 0xd4, 0x5e, + 0x78, 0x6e, 0x9b, 0x0e, 0xb9, 0xed, 0xb1, 0x60, 0xbf, 0xe1, 0x7a, 0x6f, 0x75, 0xa8, 0x1e, 0xf2, + 0x62, 0x9c, 0x7a, 0x11, 0xb1, 0xd9, 0x9d, 0xa1, 0x33, 0xf3, 0x0a, 0x5c, 0xf0, 0xa5, 0x45, 0xd3, + 0x6d, 0x6d, 0xd7, 0x9c, 0xcb, 0xd4, 0xc3, 0x1b, 0xdf, 0x17, 0x20, 0x46, 0x2f, 0x67, 0x91, 0xd3, + 0x8b, 0x13, 0x15, 0xf2, 0x85, 0xff, 0x8f, 0x52, 0x90, 0x0d, 0x5c, 0x48, 0x50, 0x6b, 0xe4, 0x9d, + 0x9d, 0x3b, 0xff, 0x3b, 0xbb, 0x8f, 0x31, 0xc2, 0x2f, 0xee, 0x23, 0x95, 0x21, 0x72, 0x8e, 0x17, + 0xe7, 0xd0, 0x03, 0x50, 0x66, 0xa2, 0x07, 0xa0, 0x4d, 0xc8, 0xf5, 0x8c, 0x31, 0x2f, 0xcc, 0xff, + 0x73, 0xe6, 0x8d, 0x6c, 0x63, 0x0a, 0x67, 0x7b, 0x81, 0x87, 0xd0, 0xd7, 0x61, 0x96, 0x61, 0x0d, + 0xaf, 0xc9, 0x0f, 0x7b, 0x3b, 0x39, 0xe5, 0x82, 0xb7, 0x31, 0x85, 0xf9, 0x5e, 0xf8, 0xf1, 0x61, + 0x03, 0xd2, 0x6f, 0xe9, 0x8a, 0x26, 0x4a, 0xb4, 0x25, 0x67, 0xbf, 0x48, 0x3c, 0x7b, 0x0a, 0xea, + 0xb0, 0x7f, 0xdf, 0x98, 0xc2, 0xf0, 0xd6, 0xb0, 0x9b, 0xdf, 0x80, 0xcc, 0xe0, 0x55, 0x5d, 0x6a, + 0x1f, 0xb0, 0x4e, 0xe5, 0x3c, 0x9c, 0xbb, 0x31, 0x85, 0xd3, 0x9e, 0x6a, 0xb1, 0x7d, 0x80, 0x36, + 0x21, 0x3b, 0x40, 0xd2, 0x1c, 0xa8, 0xf8, 0x24, 0x50, 0x03, 0x2b, 0x6a, 0x52, 0x08, 0xcb, 0x22, + 0x9a, 0xcd, 0x9a, 0x95, 0x49, 0xb1, 0x9a, 0x44, 0xb3, 0x51, 0x0b, 0x06, 0xcf, 0xff, 0xe2, 0x1e, + 0x2d, 0x8e, 0xac, 0xb2, 0x3f, 0x7f, 0x0e, 0x34, 0xb7, 0x9a, 0x6e, 0x4c, 0xe1, 0x9c, 0x1c, 0xac, + 0xaf, 0x35, 0x1f, 0xea, 0xdb, 0x4e, 0x11, 0x90, 0xd9, 0x2b, 0xc3, 0x39, 0x6d, 0x1c, 0xe0, 0xd1, + 0x0a, 0x22, 0x23, 0x1d, 0x16, 0x82, 0x78, 0xa2, 0xef, 0xe6, 0xc2, 0x7e, 0x6f, 0x5b, 0x39, 0x05, + 0x7a, 0x5c, 0x35, 0xdd, 0x98, 0xc2, 0x42, 0x60, 0x19, 0x9f, 0x90, 0xb3, 0x01, 0xef, 0x0a, 0x2b, + 0x5a, 0xba, 0xda, 0x67, 0xcf, 0x91, 0xa7, 0x6f, 0xc0, 0xbb, 0xba, 0x3a, 0x1b, 0xf0, 0xb4, 0x9b, + 0x54, 0x19, 0x6d, 0x41, 0x86, 0x11, 0x8b, 0x48, 0x59, 0xc5, 0x7d, 0x6e, 0xf8, 0xdf, 0x53, 0xc0, + 0x7c, 0x2c, 0xe5, 0xc4, 0x92, 0xe5, 0x23, 0xad, 0x8b, 0x90, 0xb2, 0x94, 0x6e, 0x4f, 0xa5, 0x9b, + 0xcf, 0xb9, 0x6d, 0xec, 0x60, 0x60, 0xc8, 0x45, 0xa5, 0x14, 0x44, 0x7a, 0x86, 0xfb, 0xab, 0xd5, + 0x1f, 0x23, 0x20, 0xb0, 0xd4, 0x63, 0x4d, 0xf1, 0xba, 0x93, 0x2f, 0xb6, 0x4d, 0x4c, 0x0b, 0xed, + 0x40, 0xa6, 0x67, 0x88, 0x7b, 0xde, 0x00, 0xe5, 0xa7, 0xdc, 0xe8, 0x6f, 0x1d, 0x61, 0x45, 0x5f, + 0xc7, 0x9a, 0xee, 0x19, 0x83, 0x61, 0x74, 0x13, 0x2e, 0xf8, 0xe1, 0x44, 0x43, 0x32, 0xa5, 0x2e, + 0x71, 0x80, 0xe9, 0x8d, 0xba, 0x94, 0x3a, 0x29, 0xc5, 0xcd, 0x98, 0xf0, 0xce, 0x47, 0x11, 0x3c, + 0xe7, 0xd3, 0x6b, 0x78, 0x62, 0xe8, 0x55, 0xa0, 0xe7, 0xef, 0xb3, 0x28, 0x3a, 0xb1, 0x45, 0x34, + 0x43, 0x86, 0x36, 0x95, 0x41, 0x08, 0x42, 0xfa, 0xac, 0x8a, 0x85, 0xad, 0xba, 0x10, 0xd0, 0x1d, + 0xd8, 0xe5, 0x6b, 0x5c, 0x7f, 0xcf, 0xc1, 0x5c, 0xa0, 0xc9, 0x61, 0x3f, 0x6e, 0x7e, 0x49, 0x64, + 0xbf, 0xe3, 0xef, 0xfb, 0x22, 0xe7, 0xee, 0xfb, 0x4a, 0x70, 0x52, 0x4a, 0xbc, 0xcf, 0xc5, 0xf8, + 0x5f, 0xfc, 0x20, 0xee, 0xeb, 0x01, 0xaf, 0xfc, 0x8a, 0x03, 0x3e, 0xec, 0x3a, 0x84, 0x20, 0xb7, + 0x5e, 0xc7, 0x3b, 0xc5, 0x56, 0xab, 0x82, 0xc5, 0x5a, 0xbd, 0x56, 0xe1, 0xa7, 0x90, 0x00, 0x73, + 0xc3, 0x31, 0x5c, 0x69, 0xd4, 0x9b, 0xd5, 0x56, 0x1d, 0xdf, 0xe3, 0x39, 0xb4, 0x00, 0x17, 0x86, + 0x33, 0xb7, 0x70, 0xa3, 0x2c, 0x36, 0x2b, 0xf8, 0x4e, 0xb5, 0x5c, 0xe1, 0x23, 0x41, 0xad, 0xcd, + 0xe2, 0x9d, 0x62, 0xb3, 0x8c, 0xab, 0x8d, 0x16, 0x1f, 0x0d, 0xce, 0x94, 0x8b, 0xf7, 0x2a, 0xb5, + 0x5a, 0x65, 0xbb, 0xd1, 0xe0, 0x63, 0x0b, 0xb3, 0x9f, 0x3c, 0x98, 0xcf, 0x0a, 0xdc, 0x95, 0xd4, + 0x60, 0xbe, 0x74, 0xfd, 0xa3, 0xbf, 0x3d, 0xcd, 0xbd, 0xbe, 0x32, 0x41, 0xfb, 0x61, 0x6b, 0xc6, + 0xee, 0x6e, 0x9c, 0x16, 0xb1, 0x6b, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x35, 0x53, 0xb3, 0xcc, + 0x91, 0x23, 0x00, 0x00, } diff --git a/pkg/ttnpb/messages.pb.paths.fm.go b/pkg/ttnpb/messages.pb.paths.fm.go index 0332ed7dbd..8d29c93875 100644 --- a/pkg/ttnpb/messages.pb.paths.fm.go +++ b/pkg/ttnpb/messages.pb.paths.fm.go @@ -556,6 +556,8 @@ var ApplicationUplinkFieldPathsNested = []string{ "network_ids.net_id", "network_ids.tenant_address", "network_ids.tenant_id", + "normalized_payload", + "normalized_payload_warnings", "received_at", "rx_metadata", "session_key_id", @@ -604,6 +606,75 @@ var ApplicationUplinkFieldPathsTopLevel = []string{ "last_a_f_cnt_down", "locations", "network_ids", + "normalized_payload", + "normalized_payload_warnings", + "received_at", + "rx_metadata", + "session_key_id", + "settings", + "version_ids", +} +var ApplicationUplinkNormalizedFieldPathsNested = []string{ + "confirmed", + "consumed_airtime", + "f_cnt", + "f_port", + "frm_payload", + "locations", + "network_ids", + "network_ids.cluster_address", + "network_ids.cluster_id", + "network_ids.net_id", + "network_ids.tenant_address", + "network_ids.tenant_id", + "normalized_payload", + "normalized_payload_warnings", + "received_at", + "rx_metadata", + "session_key_id", + "settings", + "settings.coding_rate", + "settings.concentrator_timestamp", + "settings.data_rate", + "settings.data_rate.modulation", + "settings.data_rate.modulation.fsk", + "settings.data_rate.modulation.fsk.bit_rate", + "settings.data_rate.modulation.lora", + "settings.data_rate.modulation.lora.bandwidth", + "settings.data_rate.modulation.lora.spreading_factor", + "settings.data_rate.modulation.lrfhss", + "settings.data_rate.modulation.lrfhss.coding_rate", + "settings.data_rate.modulation.lrfhss.modulation_type", + "settings.data_rate.modulation.lrfhss.operating_channel_width", + "settings.downlink", + "settings.downlink.antenna_index", + "settings.downlink.invert_polarization", + "settings.downlink.tx_power", + "settings.enable_crc", + "settings.frequency", + "settings.time", + "settings.timestamp", + "version_ids", + "version_ids.band_id", + "version_ids.brand_id", + "version_ids.firmware_version", + "version_ids.hardware_version", + "version_ids.model_id", + "version_ids.serial_number", + "version_ids.vendor_id", + "version_ids.vendor_profile_id", +} + +var ApplicationUplinkNormalizedFieldPathsTopLevel = []string{ + "confirmed", + "consumed_airtime", + "f_cnt", + "f_port", + "frm_payload", + "locations", + "network_ids", + "normalized_payload", + "normalized_payload_warnings", "received_at", "rx_metadata", "session_key_id", @@ -886,6 +957,8 @@ var ApplicationUpFieldPathsNested = []string{ "up.uplink_message.network_ids.net_id", "up.uplink_message.network_ids.tenant_address", "up.uplink_message.network_ids.tenant_id", + "up.uplink_message.normalized_payload", + "up.uplink_message.normalized_payload_warnings", "up.uplink_message.received_at", "up.uplink_message.rx_metadata", "up.uplink_message.session_key_id", @@ -920,6 +993,55 @@ var ApplicationUpFieldPathsNested = []string{ "up.uplink_message.version_ids.serial_number", "up.uplink_message.version_ids.vendor_id", "up.uplink_message.version_ids.vendor_profile_id", + "up.uplink_normalized", + "up.uplink_normalized.confirmed", + "up.uplink_normalized.consumed_airtime", + "up.uplink_normalized.f_cnt", + "up.uplink_normalized.f_port", + "up.uplink_normalized.frm_payload", + "up.uplink_normalized.locations", + "up.uplink_normalized.network_ids", + "up.uplink_normalized.network_ids.cluster_address", + "up.uplink_normalized.network_ids.cluster_id", + "up.uplink_normalized.network_ids.net_id", + "up.uplink_normalized.network_ids.tenant_address", + "up.uplink_normalized.network_ids.tenant_id", + "up.uplink_normalized.normalized_payload", + "up.uplink_normalized.normalized_payload_warnings", + "up.uplink_normalized.received_at", + "up.uplink_normalized.rx_metadata", + "up.uplink_normalized.session_key_id", + "up.uplink_normalized.settings", + "up.uplink_normalized.settings.coding_rate", + "up.uplink_normalized.settings.concentrator_timestamp", + "up.uplink_normalized.settings.data_rate", + "up.uplink_normalized.settings.data_rate.modulation", + "up.uplink_normalized.settings.data_rate.modulation.fsk", + "up.uplink_normalized.settings.data_rate.modulation.fsk.bit_rate", + "up.uplink_normalized.settings.data_rate.modulation.lora", + "up.uplink_normalized.settings.data_rate.modulation.lora.bandwidth", + "up.uplink_normalized.settings.data_rate.modulation.lora.spreading_factor", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss.coding_rate", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss.modulation_type", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss.operating_channel_width", + "up.uplink_normalized.settings.downlink", + "up.uplink_normalized.settings.downlink.antenna_index", + "up.uplink_normalized.settings.downlink.invert_polarization", + "up.uplink_normalized.settings.downlink.tx_power", + "up.uplink_normalized.settings.enable_crc", + "up.uplink_normalized.settings.frequency", + "up.uplink_normalized.settings.time", + "up.uplink_normalized.settings.timestamp", + "up.uplink_normalized.version_ids", + "up.uplink_normalized.version_ids.band_id", + "up.uplink_normalized.version_ids.brand_id", + "up.uplink_normalized.version_ids.firmware_version", + "up.uplink_normalized.version_ids.hardware_version", + "up.uplink_normalized.version_ids.model_id", + "up.uplink_normalized.version_ids.serial_number", + "up.uplink_normalized.version_ids.vendor_id", + "up.uplink_normalized.version_ids.vendor_profile_id", } var ApplicationUpFieldPathsTopLevel = []string{ diff --git a/pkg/ttnpb/messages.pb.setters.fm.go b/pkg/ttnpb/messages.pb.setters.fm.go index 67cde2d21c..a638bdeca5 100644 --- a/pkg/ttnpb/messages.pb.setters.fm.go +++ b/pkg/ttnpb/messages.pb.setters.fm.go @@ -522,6 +522,24 @@ func (dst *ApplicationUplink) SetFields(src *ApplicationUplink, paths ...string) } else { dst.DecodedPayloadWarnings = nil } + case "normalized_payload": + if len(subs) > 0 { + return fmt.Errorf("'normalized_payload' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.NormalizedPayload = src.NormalizedPayload + } else { + dst.NormalizedPayload = nil + } + case "normalized_payload_warnings": + if len(subs) > 0 { + return fmt.Errorf("'normalized_payload_warnings' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.NormalizedPayloadWarnings = src.NormalizedPayloadWarnings + } else { + dst.NormalizedPayloadWarnings = nil + } case "rx_metadata": if len(subs) > 0 { return fmt.Errorf("'rx_metadata' has no subfields, but %s were specified", subs) @@ -686,6 +704,194 @@ func (dst *ApplicationUplink) SetFields(src *ApplicationUplink, paths ...string) return nil } +func (dst *ApplicationUplinkNormalized) SetFields(src *ApplicationUplinkNormalized, paths ...string) error { + for name, subs := range _processPaths(paths) { + switch name { + case "session_key_id": + if len(subs) > 0 { + return fmt.Errorf("'session_key_id' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.SessionKeyId = src.SessionKeyId + } else { + dst.SessionKeyId = nil + } + case "f_port": + if len(subs) > 0 { + return fmt.Errorf("'f_port' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FPort = src.FPort + } else { + var zero uint32 + dst.FPort = zero + } + case "f_cnt": + if len(subs) > 0 { + return fmt.Errorf("'f_cnt' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FCnt = src.FCnt + } else { + var zero uint32 + dst.FCnt = zero + } + case "frm_payload": + if len(subs) > 0 { + return fmt.Errorf("'frm_payload' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.FrmPayload = src.FrmPayload + } else { + dst.FrmPayload = nil + } + case "normalized_payload": + if len(subs) > 0 { + return fmt.Errorf("'normalized_payload' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.NormalizedPayload = src.NormalizedPayload + } else { + dst.NormalizedPayload = nil + } + case "normalized_payload_warnings": + if len(subs) > 0 { + return fmt.Errorf("'normalized_payload_warnings' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.NormalizedPayloadWarnings = src.NormalizedPayloadWarnings + } else { + dst.NormalizedPayloadWarnings = nil + } + case "rx_metadata": + if len(subs) > 0 { + return fmt.Errorf("'rx_metadata' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.RxMetadata = src.RxMetadata + } else { + dst.RxMetadata = nil + } + case "settings": + if len(subs) > 0 { + var newDst, newSrc *TxSettings + if (src == nil || src.Settings == nil) && dst.Settings == nil { + continue + } + if src != nil { + newSrc = src.Settings + } + if dst.Settings != nil { + newDst = dst.Settings + } else { + newDst = &TxSettings{} + dst.Settings = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.Settings = src.Settings + } else { + dst.Settings = nil + } + } + case "received_at": + if len(subs) > 0 { + return fmt.Errorf("'received_at' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.ReceivedAt = src.ReceivedAt + } else { + dst.ReceivedAt = nil + } + case "confirmed": + if len(subs) > 0 { + return fmt.Errorf("'confirmed' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.Confirmed = src.Confirmed + } else { + var zero bool + dst.Confirmed = zero + } + case "consumed_airtime": + if len(subs) > 0 { + return fmt.Errorf("'consumed_airtime' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.ConsumedAirtime = src.ConsumedAirtime + } else { + dst.ConsumedAirtime = nil + } + case "locations": + if len(subs) > 0 { + return fmt.Errorf("'locations' has no subfields, but %s were specified", subs) + } + if src != nil { + dst.Locations = src.Locations + } else { + dst.Locations = nil + } + case "version_ids": + if len(subs) > 0 { + var newDst, newSrc *EndDeviceVersionIdentifiers + if (src == nil || src.VersionIds == nil) && dst.VersionIds == nil { + continue + } + if src != nil { + newSrc = src.VersionIds + } + if dst.VersionIds != nil { + newDst = dst.VersionIds + } else { + newDst = &EndDeviceVersionIdentifiers{} + dst.VersionIds = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.VersionIds = src.VersionIds + } else { + dst.VersionIds = nil + } + } + case "network_ids": + if len(subs) > 0 { + var newDst, newSrc *NetworkIdentifiers + if (src == nil || src.NetworkIds == nil) && dst.NetworkIds == nil { + continue + } + if src != nil { + newSrc = src.NetworkIds + } + if dst.NetworkIds != nil { + newDst = dst.NetworkIds + } else { + newDst = &NetworkIdentifiers{} + dst.NetworkIds = newDst + } + if err := newDst.SetFields(newSrc, subs...); err != nil { + return err + } + } else { + if src != nil { + dst.NetworkIds = src.NetworkIds + } else { + dst.NetworkIds = nil + } + } + + default: + return fmt.Errorf("invalid field: '%s'", name) + } + } + return nil +} + func (dst *ApplicationLocation) SetFields(src *ApplicationLocation, paths ...string) error { for name, subs := range _processPaths(paths) { switch name { @@ -1260,6 +1466,42 @@ func (dst *ApplicationUp) SetFields(src *ApplicationUp, paths ...string) error { dst.Up = nil } } + case "uplink_normalized": + var srcTypeOk bool + if src != nil { + _, srcTypeOk = src.Up.(*ApplicationUp_UplinkNormalized) + } + if srcValid := srcTypeOk || src == nil || src.Up == nil || len(oneofSubs) == 0; !srcValid { + return fmt.Errorf("attempt to set oneof 'uplink_normalized', while different oneof is set in source") + } + _, dstTypeOk := dst.Up.(*ApplicationUp_UplinkNormalized) + if dstValid := dstTypeOk || dst.Up == nil || len(oneofSubs) == 0; !dstValid { + return fmt.Errorf("attempt to set oneof 'uplink_normalized', while different oneof is set in destination") + } + if len(oneofSubs) > 0 { + var newDst, newSrc *ApplicationUplinkNormalized + if srcTypeOk { + newSrc = src.Up.(*ApplicationUp_UplinkNormalized).UplinkNormalized + } + if dstTypeOk { + newDst = dst.Up.(*ApplicationUp_UplinkNormalized).UplinkNormalized + } else if srcTypeOk { + newDst = &ApplicationUplinkNormalized{} + dst.Up = &ApplicationUp_UplinkNormalized{UplinkNormalized: newDst} + } else { + dst.Up = nil + continue + } + if err := newDst.SetFields(newSrc, oneofSubs...); err != nil { + return err + } + } else { + if srcTypeOk { + dst.Up = src.Up + } else { + dst.Up = nil + } + } case "join_accept": var srcTypeOk bool if src != nil { diff --git a/pkg/ttnpb/messages.pb.validate.go b/pkg/ttnpb/messages.pb.validate.go index de738c0196..d61d839b60 100644 --- a/pkg/ttnpb/messages.pb.validate.go +++ b/pkg/ttnpb/messages.pb.validate.go @@ -767,6 +767,25 @@ func (m *ApplicationUplink) ValidateFields(paths ...string) error { case "decoded_payload_warnings": + case "normalized_payload": + + for idx, item := range m.GetNormalizedPayload() { + _, _ = idx, item + + if v, ok := interface{}(item).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkValidationError{ + field: fmt.Sprintf("normalized_payload[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + case "normalized_payload_warnings": + case "rx_metadata": for idx, item := range m.GetRxMetadata() { @@ -956,6 +975,244 @@ var _ApplicationUplink_FPort_NotInLookup = map[uint32]struct{}{ 224: {}, } +// ValidateFields checks the field values on ApplicationUplinkNormalized with +// the rules defined in the proto definition for this message. If any rules +// are violated, an error is returned. +func (m *ApplicationUplinkNormalized) ValidateFields(paths ...string) error { + if m == nil { + return nil + } + + if len(paths) == 0 { + paths = ApplicationUplinkNormalizedFieldPathsNested + } + + for name, subs := range _processPaths(append(paths[:0:0], paths...)) { + _ = subs + switch name { + case "session_key_id": + + if len(m.GetSessionKeyId()) > 2048 { + return ApplicationUplinkNormalizedValidationError{ + field: "session_key_id", + reason: "value length must be at most 2048 bytes", + } + } + + case "f_port": + + if val := m.GetFPort(); val < 1 || val > 255 { + return ApplicationUplinkNormalizedValidationError{ + field: "f_port", + reason: "value must be inside range [1, 255]", + } + } + + if _, ok := _ApplicationUplinkNormalized_FPort_NotInLookup[m.GetFPort()]; ok { + return ApplicationUplinkNormalizedValidationError{ + field: "f_port", + reason: "value must not be in list [224]", + } + } + + case "f_cnt": + // no validation rules for FCnt + case "frm_payload": + // no validation rules for FrmPayload + case "normalized_payload": + + if m.GetNormalizedPayload() == nil { + return ApplicationUplinkNormalizedValidationError{ + field: "normalized_payload", + reason: "value is required", + } + } + + if v, ok := interface{}(m.GetNormalizedPayload()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: "normalized_payload", + reason: "embedded message failed validation", + cause: err, + } + } + } + + case "normalized_payload_warnings": + + case "rx_metadata": + + for idx, item := range m.GetRxMetadata() { + _, _ = idx, item + + if v, ok := interface{}(item).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: fmt.Sprintf("rx_metadata[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + case "settings": + + if m.GetSettings() == nil { + return ApplicationUplinkNormalizedValidationError{ + field: "settings", + reason: "value is required", + } + } + + if v, ok := interface{}(m.GetSettings()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: "settings", + reason: "embedded message failed validation", + cause: err, + } + } + } + + case "received_at": + + if m.GetReceivedAt() == nil { + return ApplicationUplinkNormalizedValidationError{ + field: "received_at", + reason: "value is required", + } + } + + case "confirmed": + // no validation rules for Confirmed + case "consumed_airtime": + + if v, ok := interface{}(m.GetConsumedAirtime()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: "consumed_airtime", + reason: "embedded message failed validation", + cause: err, + } + } + } + + case "locations": + + for key, val := range m.GetLocations() { + _ = val + + // no validation rules for Locations[key] + + if v, ok := interface{}(val).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: fmt.Sprintf("locations[%v]", key), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + case "version_ids": + + if v, ok := interface{}(m.GetVersionIds()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: "version_ids", + reason: "embedded message failed validation", + cause: err, + } + } + } + + case "network_ids": + + if v, ok := interface{}(m.GetNetworkIds()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUplinkNormalizedValidationError{ + field: "network_ids", + reason: "embedded message failed validation", + cause: err, + } + } + } + + default: + return ApplicationUplinkNormalizedValidationError{ + field: name, + reason: "invalid field path", + } + } + } + return nil +} + +// ApplicationUplinkNormalizedValidationError is the validation error returned +// by ApplicationUplinkNormalized.ValidateFields if the designated constraints +// aren't met. +type ApplicationUplinkNormalizedValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ApplicationUplinkNormalizedValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ApplicationUplinkNormalizedValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ApplicationUplinkNormalizedValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ApplicationUplinkNormalizedValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ApplicationUplinkNormalizedValidationError) ErrorName() string { + return "ApplicationUplinkNormalizedValidationError" +} + +// Error satisfies the builtin error interface +func (e ApplicationUplinkNormalizedValidationError) 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 %sApplicationUplinkNormalized.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ApplicationUplinkNormalizedValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ApplicationUplinkNormalizedValidationError{} + +var _ApplicationUplinkNormalized_FPort_NotInLookup = map[uint32]struct{}{ + 224: {}, +} + // ValidateFields checks the field values on ApplicationLocation with the rules // defined in the proto definition for this message. If any rules are // violated, an error is returned. @@ -1152,13 +1409,10 @@ func (m *ApplicationJoinAccept) ValidateFields(paths ...string) error { // no validation rules for PendingSession case "received_at": - if v, ok := interface{}(m.GetReceivedAt()).(interface{ ValidateFields(...string) error }); ok { - if err := v.ValidateFields(subs...); err != nil { - return ApplicationJoinAcceptValidationError{ - field: "received_at", - reason: "embedded message failed validation", - cause: err, - } + if m.GetReceivedAt() == nil { + return ApplicationJoinAcceptValidationError{ + field: "received_at", + reason: "value is required", } } @@ -2017,7 +2271,7 @@ func (m *ApplicationUp) ValidateFields(paths ...string) error { } if len(subs) == 0 { subs = []string{ - "uplink_message", "join_accept", "downlink_ack", "downlink_nack", "downlink_sent", "downlink_failed", "downlink_queued", "downlink_queue_invalidated", "location_solved", "service_data", + "uplink_message", "uplink_normalized", "join_accept", "downlink_ack", "downlink_nack", "downlink_sent", "downlink_failed", "downlink_queued", "downlink_queue_invalidated", "location_solved", "service_data", } } for name, subs := range _processPaths(subs) { @@ -2039,6 +2293,22 @@ func (m *ApplicationUp) ValidateFields(paths ...string) error { } } + case "uplink_normalized": + w, ok := m.Up.(*ApplicationUp_UplinkNormalized) + if !ok || w == nil { + continue + } + + if v, ok := interface{}(m.GetUplinkNormalized()).(interface{ ValidateFields(...string) error }); ok { + if err := v.ValidateFields(subs...); err != nil { + return ApplicationUpValidationError{ + field: "uplink_normalized", + reason: "embedded message failed validation", + cause: err, + } + } + } + case "join_accept": w, ok := m.Up.(*ApplicationUp_JoinAccept) if !ok || w == nil { diff --git a/pkg/ttnpb/messages_flags.pb.go b/pkg/ttnpb/messages_flags.pb.go index f1877f5600..02c8bfc3ae 100644 --- a/pkg/ttnpb/messages_flags.pb.go +++ b/pkg/ttnpb/messages_flags.pb.go @@ -20,6 +20,8 @@ func AddSelectFlagsForApplicationUplink(flags *pflag.FlagSet, prefix string, hid flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("frm-payload", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("frm-payload", prefix), false), flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("decoded-payload", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("decoded-payload", prefix), false), flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("decoded-payload-warnings", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("decoded-payload-warnings", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("normalized-payload", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("normalized-payload", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("normalized-payload-warnings", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("normalized-payload-warnings", prefix), false), flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("rx-metadata", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("rx-metadata", prefix), false), flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("settings", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("settings", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForTxSettings(flags, flagsplugin.Prefix("settings", prefix), hidden) @@ -68,6 +70,16 @@ func PathsFromSelectFlagsForApplicationUplink(flags *pflag.FlagSet, prefix strin } else if selected && val { paths = append(paths, flagsplugin.Prefix("decoded_payload_warnings", prefix)) } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("normalized_payload", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("normalized_payload", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("normalized_payload_warnings", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("normalized_payload_warnings", prefix)) + } if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("rx_metadata", prefix)); err != nil { return nil, err } else if selected && val { @@ -141,6 +153,117 @@ func PathsFromSelectFlagsForApplicationUplink(flags *pflag.FlagSet, prefix strin return paths, nil } +// AddSelectFlagsForApplicationUplinkNormalized adds flags to select fields in ApplicationUplinkNormalized. +func AddSelectFlagsForApplicationUplinkNormalized(flags *pflag.FlagSet, prefix string, hidden bool) { + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("session-key-id", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("session-key-id", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("f-port", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("f-port", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("f-cnt", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("f-cnt", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("frm-payload", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("frm-payload", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("normalized-payload", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("normalized-payload", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("normalized-payload-warnings", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("normalized-payload-warnings", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("rx-metadata", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("rx-metadata", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("settings", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("settings", prefix), true), flagsplugin.WithHidden(hidden))) + AddSelectFlagsForTxSettings(flags, flagsplugin.Prefix("settings", prefix), hidden) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("received-at", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("received-at", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("confirmed", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("confirmed", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("consumed-airtime", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("consumed-airtime", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("locations", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("locations", prefix), false), flagsplugin.WithHidden(hidden))) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("version-ids", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("version-ids", prefix), true), flagsplugin.WithHidden(hidden))) + AddSelectFlagsForEndDeviceVersionIdentifiers(flags, flagsplugin.Prefix("version-ids", prefix), hidden) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("network-ids", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("network-ids", prefix), true), flagsplugin.WithHidden(hidden))) + AddSelectFlagsForNetworkIdentifiers(flags, flagsplugin.Prefix("network-ids", prefix), hidden) +} + +// SelectFromFlags outputs the fieldmask paths forApplicationUplinkNormalized message from select flags. +func PathsFromSelectFlagsForApplicationUplinkNormalized(flags *pflag.FlagSet, prefix string) (paths []string, err error) { + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("session_key_id", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("session_key_id", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("f_port", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("f_port", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("f_cnt", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("f_cnt", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("frm_payload", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("frm_payload", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("normalized_payload", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("normalized_payload", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("normalized_payload_warnings", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("normalized_payload_warnings", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("rx_metadata", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("rx_metadata", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("settings", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("settings", prefix)) + } + if selectPaths, err := PathsFromSelectFlagsForTxSettings(flags, flagsplugin.Prefix("settings", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, selectPaths...) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("received_at", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("received_at", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("confirmed", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("confirmed", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("consumed_airtime", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("consumed_airtime", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("locations", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("locations", prefix)) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("version_ids", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("version_ids", prefix)) + } + if selectPaths, err := PathsFromSelectFlagsForEndDeviceVersionIdentifiers(flags, flagsplugin.Prefix("version_ids", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, selectPaths...) + } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("network_ids", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("network_ids", prefix)) + } + if selectPaths, err := PathsFromSelectFlagsForNetworkIdentifiers(flags, flagsplugin.Prefix("network_ids", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, selectPaths...) + } + return paths, nil +} + // AddSelectFlagsForApplicationLocation adds flags to select fields in ApplicationLocation. func AddSelectFlagsForApplicationLocation(flags *pflag.FlagSet, prefix string, hidden bool) { flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("service", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("service", prefix), false), flagsplugin.WithHidden(hidden))) @@ -500,6 +623,8 @@ func AddSelectFlagsForApplicationUp(flags *pflag.FlagSet, prefix string, hidden flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("received-at", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("received-at", prefix), false), flagsplugin.WithHidden(hidden))) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("up.uplink-message", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("up.uplink-message", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForApplicationUplink(flags, flagsplugin.Prefix("up.uplink-message", prefix), hidden) + flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("up.uplink-normalized", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("up.uplink-normalized", prefix), true), flagsplugin.WithHidden(hidden))) + AddSelectFlagsForApplicationUplinkNormalized(flags, flagsplugin.Prefix("up.uplink-normalized", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("up.join-accept", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("up.join-accept", prefix), true), flagsplugin.WithHidden(hidden))) AddSelectFlagsForApplicationJoinAccept(flags, flagsplugin.Prefix("up.join-accept", prefix), hidden) flags.AddFlag(flagsplugin.NewBoolFlag(flagsplugin.Prefix("up.downlink-ack", prefix), flagsplugin.SelectDesc(flagsplugin.Prefix("up.downlink-ack", prefix), true), flagsplugin.WithHidden(hidden))) @@ -553,6 +678,16 @@ func PathsFromSelectFlagsForApplicationUp(flags *pflag.FlagSet, prefix string) ( } else { paths = append(paths, selectPaths...) } + if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("up.uplink_normalized", prefix)); err != nil { + return nil, err + } else if selected && val { + paths = append(paths, flagsplugin.Prefix("up.uplink_normalized", prefix)) + } + if selectPaths, err := PathsFromSelectFlagsForApplicationUplinkNormalized(flags, flagsplugin.Prefix("up.uplink_normalized", prefix)); err != nil { + return nil, err + } else { + paths = append(paths, selectPaths...) + } if val, selected, err := flagsplugin.GetBool(flags, flagsplugin.Prefix("up.join_accept", prefix)); err != nil { return nil, err } else if selected && val { diff --git a/pkg/ttnpb/messages_json.pb.go b/pkg/ttnpb/messages_json.pb.go index 38b743ac1b..f993c85f89 100644 --- a/pkg/ttnpb/messages_json.pb.go +++ b/pkg/ttnpb/messages_json.pb.go @@ -600,6 +600,26 @@ func (x *ApplicationUplink) MarshalProtoJSON(s *jsonplugin.MarshalState) { s.WriteObjectField("decoded_payload_warnings") s.WriteStringArray(x.DecodedPayloadWarnings) } + if len(x.NormalizedPayload) > 0 || s.HasField("normalized_payload") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("normalized_payload") + s.WriteArrayStart() + var wroteElement bool + for _, element := range x.NormalizedPayload { + s.WriteMoreIf(&wroteElement) + if element == nil { + s.WriteNil() + } else { + gogo.MarshalStruct(s, element) + } + } + s.WriteArrayEnd() + } + if len(x.NormalizedPayloadWarnings) > 0 || s.HasField("normalized_payload_warnings") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("normalized_payload_warnings") + s.WriteStringArray(x.NormalizedPayloadWarnings) + } if len(x.RxMetadata) > 0 || s.HasField("rx_metadata") { s.WriteMoreIf(&wroteField) s.WriteObjectField("rx_metadata") @@ -720,6 +740,26 @@ func (x *ApplicationUplink) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { return } x.DecodedPayloadWarnings = s.ReadStringArray() + case "normalized_payload", "normalizedPayload": + s.AddField("normalized_payload") + if s.ReadNil() { + x.NormalizedPayload = nil + return + } + s.ReadArray(func() { + v := gogo.UnmarshalStruct(s) + if s.Err() != nil { + return + } + x.NormalizedPayload = append(x.NormalizedPayload, v) + }) + case "normalized_payload_warnings", "normalizedPayloadWarnings": + s.AddField("normalized_payload_warnings") + if s.ReadNil() { + x.NormalizedPayloadWarnings = nil + return + } + x.NormalizedPayloadWarnings = s.ReadStringArray() case "rx_metadata", "rxMetadata": s.AddField("rx_metadata") if s.ReadNil() { @@ -821,6 +861,249 @@ func (x *ApplicationUplink) UnmarshalJSON(b []byte) error { return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) } +// MarshalProtoJSON marshals the ApplicationUplinkNormalized message to JSON. +func (x *ApplicationUplinkNormalized) MarshalProtoJSON(s *jsonplugin.MarshalState) { + if x == nil { + s.WriteNil() + return + } + s.WriteObjectStart() + var wroteField bool + if len(x.SessionKeyId) > 0 || s.HasField("session_key_id") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("session_key_id") + s.WriteBytes(x.SessionKeyId) + } + if x.FPort != 0 || s.HasField("f_port") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("f_port") + s.WriteUint32(x.FPort) + } + if x.FCnt != 0 || s.HasField("f_cnt") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("f_cnt") + s.WriteUint32(x.FCnt) + } + if len(x.FrmPayload) > 0 || s.HasField("frm_payload") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("frm_payload") + s.WriteBytes(x.FrmPayload) + } + if x.NormalizedPayload != nil || s.HasField("normalized_payload") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("normalized_payload") + if x.NormalizedPayload == nil { + s.WriteNil() + } else { + gogo.MarshalStruct(s, x.NormalizedPayload) + } + } + if len(x.NormalizedPayloadWarnings) > 0 || s.HasField("normalized_payload_warnings") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("normalized_payload_warnings") + s.WriteStringArray(x.NormalizedPayloadWarnings) + } + if len(x.RxMetadata) > 0 || s.HasField("rx_metadata") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("rx_metadata") + s.WriteArrayStart() + var wroteElement bool + for _, element := range x.RxMetadata { + s.WriteMoreIf(&wroteElement) + element.MarshalProtoJSON(s.WithField("rx_metadata")) + } + s.WriteArrayEnd() + } + if x.Settings != nil || s.HasField("settings") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("settings") + // NOTE: TxSettings does not seem to implement MarshalProtoJSON. + gogo.MarshalMessage(s, x.Settings) + } + if x.ReceivedAt != nil || s.HasField("received_at") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("received_at") + if x.ReceivedAt == nil { + s.WriteNil() + } else { + gogo.MarshalTimestamp(s, x.ReceivedAt) + } + } + if x.Confirmed || s.HasField("confirmed") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("confirmed") + s.WriteBool(x.Confirmed) + } + if x.ConsumedAirtime != nil || s.HasField("consumed_airtime") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("consumed_airtime") + if x.ConsumedAirtime == nil { + s.WriteNil() + } else { + gogo.MarshalDuration(s, x.ConsumedAirtime) + } + } + if x.Locations != nil || s.HasField("locations") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("locations") + s.WriteObjectStart() + var wroteElement bool + for k, v := range x.Locations { + s.WriteMoreIf(&wroteElement) + s.WriteObjectStringField(k) + v.MarshalProtoJSON(s.WithField("locations")) + } + s.WriteObjectEnd() + } + if x.VersionIds != nil || s.HasField("version_ids") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("version_ids") + // NOTE: EndDeviceVersionIdentifiers does not seem to implement MarshalProtoJSON. + gogo.MarshalMessage(s, x.VersionIds) + } + if x.NetworkIds != nil || s.HasField("network_ids") { + s.WriteMoreIf(&wroteField) + s.WriteObjectField("network_ids") + x.NetworkIds.MarshalProtoJSON(s.WithField("network_ids")) + } + s.WriteObjectEnd() +} + +// MarshalJSON marshals the ApplicationUplinkNormalized to JSON. +func (x *ApplicationUplinkNormalized) MarshalJSON() ([]byte, error) { + return jsonplugin.DefaultMarshalerConfig.Marshal(x) +} + +// UnmarshalProtoJSON unmarshals the ApplicationUplinkNormalized message from JSON. +func (x *ApplicationUplinkNormalized) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { + if s.ReadNil() { + return + } + s.ReadObject(func(key string) { + switch key { + default: + s.ReadAny() // ignore unknown field + case "session_key_id", "sessionKeyId": + s.AddField("session_key_id") + x.SessionKeyId = s.ReadBytes() + case "f_port", "fPort": + s.AddField("f_port") + x.FPort = s.ReadUint32() + case "f_cnt", "fCnt": + s.AddField("f_cnt") + x.FCnt = s.ReadUint32() + case "frm_payload", "frmPayload": + s.AddField("frm_payload") + x.FrmPayload = s.ReadBytes() + case "normalized_payload", "normalizedPayload": + s.AddField("normalized_payload") + if s.ReadNil() { + x.NormalizedPayload = nil + return + } + v := gogo.UnmarshalStruct(s) + if s.Err() != nil { + return + } + x.NormalizedPayload = v + case "normalized_payload_warnings", "normalizedPayloadWarnings": + s.AddField("normalized_payload_warnings") + if s.ReadNil() { + x.NormalizedPayloadWarnings = nil + return + } + x.NormalizedPayloadWarnings = s.ReadStringArray() + case "rx_metadata", "rxMetadata": + s.AddField("rx_metadata") + if s.ReadNil() { + x.RxMetadata = nil + return + } + s.ReadArray(func() { + if s.ReadNil() { + x.RxMetadata = append(x.RxMetadata, nil) + return + } + v := &RxMetadata{} + v.UnmarshalProtoJSON(s.WithField("rx_metadata", false)) + if s.Err() != nil { + return + } + x.RxMetadata = append(x.RxMetadata, v) + }) + case "settings": + s.AddField("settings") + if s.ReadNil() { + x.Settings = nil + return + } + // NOTE: TxSettings does not seem to implement UnmarshalProtoJSON. + var v TxSettings + gogo.UnmarshalMessage(s, &v) + x.Settings = &v + case "received_at", "receivedAt": + s.AddField("received_at") + if s.ReadNil() { + x.ReceivedAt = nil + return + } + v := gogo.UnmarshalTimestamp(s) + if s.Err() != nil { + return + } + x.ReceivedAt = v + case "confirmed": + s.AddField("confirmed") + x.Confirmed = s.ReadBool() + case "consumed_airtime", "consumedAirtime": + s.AddField("consumed_airtime") + if s.ReadNil() { + x.ConsumedAirtime = nil + return + } + v := gogo.UnmarshalDuration(s) + if s.Err() != nil { + return + } + x.ConsumedAirtime = v + case "locations": + s.AddField("locations") + if s.ReadNil() { + x.Locations = nil + return + } + x.Locations = make(map[string]*Location) + s.ReadStringMap(func(key string) { + var v Location + v.UnmarshalProtoJSON(s) + x.Locations[key] = &v + }) + case "version_ids", "versionIds": + s.AddField("version_ids") + if s.ReadNil() { + x.VersionIds = nil + return + } + // NOTE: EndDeviceVersionIdentifiers does not seem to implement UnmarshalProtoJSON. + var v EndDeviceVersionIdentifiers + gogo.UnmarshalMessage(s, &v) + x.VersionIds = &v + case "network_ids", "networkIds": + if s.ReadNil() { + x.NetworkIds = nil + return + } + x.NetworkIds = &NetworkIdentifiers{} + x.NetworkIds.UnmarshalProtoJSON(s.WithField("network_ids", true)) + } + }) +} + +// UnmarshalJSON unmarshals the ApplicationUplinkNormalized from JSON. +func (x *ApplicationUplinkNormalized) UnmarshalJSON(b []byte) error { + return jsonplugin.DefaultUnmarshalerConfig.Unmarshal(b, x) +} + // MarshalProtoJSON marshals the ApplicationLocation message to JSON. func (x *ApplicationLocation) MarshalProtoJSON(s *jsonplugin.MarshalState) { if x == nil { @@ -1548,6 +1831,10 @@ func (x *ApplicationUp) MarshalProtoJSON(s *jsonplugin.MarshalState) { s.WriteMoreIf(&wroteField) s.WriteObjectField("uplink_message") ov.UplinkMessage.MarshalProtoJSON(s.WithField("uplink_message")) + case *ApplicationUp_UplinkNormalized: + s.WriteMoreIf(&wroteField) + s.WriteObjectField("uplink_normalized") + ov.UplinkNormalized.MarshalProtoJSON(s.WithField("uplink_normalized")) case *ApplicationUp_JoinAccept: s.WriteMoreIf(&wroteField) s.WriteObjectField("join_accept") @@ -1643,6 +1930,15 @@ func (x *ApplicationUp) UnmarshalProtoJSON(s *jsonplugin.UnmarshalState) { } ov.UplinkMessage = &ApplicationUplink{} ov.UplinkMessage.UnmarshalProtoJSON(s.WithField("uplink_message", true)) + case "uplink_normalized", "uplinkNormalized": + ov := &ApplicationUp_UplinkNormalized{} + x.Up = ov + if s.ReadNil() { + ov.UplinkNormalized = nil + return + } + ov.UplinkNormalized = &ApplicationUplinkNormalized{} + ov.UplinkNormalized.UnmarshalProtoJSON(s.WithField("uplink_normalized", true)) case "join_accept", "joinAccept": ov := &ApplicationUp_JoinAccept{} x.Up = ov diff --git a/pkg/webui/console/components/events/previews/application-uplink-normalized.js b/pkg/webui/console/components/events/previews/application-uplink-normalized.js new file mode 100644 index 0000000000..a390eb46a2 --- /dev/null +++ b/pkg/webui/console/components/events/previews/application-uplink-normalized.js @@ -0,0 +1,58 @@ +// Copyright © 2022 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. + +import React from 'react' + +import { getDataRate, getSignalInformation } from '@console/components/events/utils' + +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import messages from '../messages' + +import DescriptionList from './shared/description-list' +import JSONPayload from './shared/json-payload' + +const ApplicationUplinkNormalizedPreview = React.memo(({ event }) => { + const { data, identifiers } = event + const deviceIds = identifiers[0].device_ids + const { snr, rssi } = getSignalInformation(data) + const dataRate = getDataRate(data) + + return ( + + + {data.normalized_payload.air && ( + + + + )} + {data.normalized_payload.wind && ( + + + + )} + + + + + + ) +}) + +ApplicationUplinkNormalizedPreview.propTypes = { + event: PropTypes.event.isRequired, +} + +export default ApplicationUplinkNormalizedPreview diff --git a/pkg/webui/console/components/events/utils/definitions.js b/pkg/webui/console/components/events/utils/definitions.js index e6d5e1dd24..6870527bf0 100644 --- a/pkg/webui/console/components/events/utils/definitions.js +++ b/pkg/webui/console/components/events/utils/definitions.js @@ -16,6 +16,7 @@ import DownlinkMessage from '../previews/downlink-message' import GatewayUplinkMessage from '../previews/gateway-uplink-message' import ApplicationDownlink from '../previews/application-downlink' import ApplicationUplink from '../previews/application-uplink' +import ApplicationUplinkNormalized from '../previews/application-uplink-normalized' import ApplicationUp from '../previews/application-up' import ApplicationLocation from '../previews/application-location' import JoinRequest from '../previews/join-request' @@ -98,6 +99,7 @@ export const eventIconMap = [ export const dataTypeMap = { ApplicationDownlink, ApplicationUplink, + ApplicationUplinkNormalized, ApplicationUp, ApplicationLocation, DownlinkMessage, @@ -111,6 +113,7 @@ export const dataTypeMap = { export const applicationUpMessages = [ 'uplink_message', + 'uplink_normalized', 'join_accept', 'downlink_ack', 'downlink_nack', diff --git a/pkg/webui/console/components/events/utils/index.js b/pkg/webui/console/components/events/utils/index.js index e625467c6d..867019879f 100644 --- a/pkg/webui/console/components/events/utils/index.js +++ b/pkg/webui/console/components/events/utils/index.js @@ -68,6 +68,9 @@ export const getPreviewComponentByApplicationUpMessage = message => { case 'uplink_message': messageType = 'ApplicationUplink' break + case 'uplink_normalized': + messageType = 'ApplicationUplinkNormalized' + break case 'join_accept': messageType = 'ApplicationJoinAccept' break @@ -86,6 +89,7 @@ export const getPreviewComponentByApplicationUpMessage = message => { break case 'service_data': messageType = 'ApplicationServiceData' + break } return messageType in dataTypeMap ? dataTypeMap[messageType] : DefaultPreview diff --git a/pkg/webui/console/components/pubsub-form/index.js b/pkg/webui/console/components/pubsub-form/index.js index 5fa0a6175f..1db0e11d68 100644 --- a/pkg/webui/console/components/pubsub-form/index.js +++ b/pkg/webui/console/components/pubsub-form/index.js @@ -355,6 +355,14 @@ export default class PubsubForm extends Component { component={Input.Toggled} description={sharedMessages.eventUplinkMessageDesc} /> + { location_solved: mapPubsubMessageTypeToFormValue(pubsub.location_solved), service_data: mapPubsubMessageTypeToFormValue(pubsub.service_data), uplink_message: mapPubsubMessageTypeToFormValue(pubsub.uplink_message), + uplink_normalized: mapPubsubMessageTypeToFormValue(pubsub.uplink_normalized), } return result @@ -137,6 +138,7 @@ export const mapFormValuesToPubsub = (values, appId) => { location_solved: mapMessageTypeFormValueToPubsubMessageType(values.location_solved), service_data: mapMessageTypeFormValueToPubsubMessageType(values.service_data), uplink_message: mapMessageTypeFormValueToPubsubMessageType(values.uplink_message), + uplink_normalized: mapMessageTypeFormValueToPubsubMessageType(values.uplink_normalized), } switch (values._provider) { @@ -172,4 +174,5 @@ export const blankValues = { location_solved: { enabled: false, value: '' }, service_data: { enabled: false, value: '' }, uplink_message: { enabled: false, value: '' }, + uplink_normalized: { enabled: false, value: '' }, } diff --git a/pkg/webui/console/components/webhook-form/index.js b/pkg/webui/console/components/webhook-form/index.js index 07b352203a..7b8d4def53 100644 --- a/pkg/webui/console/components/webhook-form/index.js +++ b/pkg/webui/console/components/webhook-form/index.js @@ -168,6 +168,12 @@ const validationSchema = Yup.object().shape({ }) .test('has path length at most 64 characters', m.messagePathValidateTooLong, messageCheck) .nullable(), + uplink_normalized: Yup.object() + .shape({ + path: Yup.string(), + }) + .test('has path length at most 64 characters', m.messagePathValidateTooLong, messageCheck) + .nullable(), join_accept: Yup.object() .shape({ path: Yup.string(), @@ -516,6 +522,16 @@ export default class WebhookForm extends Component { component={Input.Toggled} description={sharedMessages.eventUplinkMessageDesc} /> + { template_fields: fields, base_url: urlTemplate.parse(template.base_url).expand(fields), uplink_message: pathExpand(template.uplink_message, fields), + uplink_normalized: pathExpand(template.uplink_normalized, fields), join_accept: pathExpand(template.join_accept, fields), downlink_ack: pathExpand(template.downlink_ack, fields), downlink_nack: pathExpand(template.downlink_nack, fields), diff --git a/pkg/webui/console/views/application-integrations-pubsub-edit/connect.js b/pkg/webui/console/views/application-integrations-pubsub-edit/connect.js index a52eff280a..6ea7824986 100644 --- a/pkg/webui/console/views/application-integrations-pubsub-edit/connect.js +++ b/pkg/webui/console/views/application-integrations-pubsub-edit/connect.js @@ -49,6 +49,7 @@ const pubsubEntitySelector = [ 'location_solved', 'service_data', 'uplink_message', + 'uplink_normalized', ] const mapStateToProps = state => ({ diff --git a/pkg/webui/console/views/application-integrations-webhook-edit/connect.js b/pkg/webui/console/views/application-integrations-webhook-edit/connect.js index ffbed7df83..941ce5a9e9 100644 --- a/pkg/webui/console/views/application-integrations-webhook-edit/connect.js +++ b/pkg/webui/console/views/application-integrations-webhook-edit/connect.js @@ -35,6 +35,7 @@ const webhookEntitySelector = [ 'headers', 'downlink_api_key', 'uplink_message', + 'uplink_normalized', 'join_accept', 'downlink_ack', 'downlink_nack', diff --git a/pkg/webui/console/views/application-integrations-webhooks/connect.js b/pkg/webui/console/views/application-integrations-webhooks/connect.js index ddad4abc1c..1a8bf91841 100644 --- a/pkg/webui/console/views/application-integrations-webhooks/connect.js +++ b/pkg/webui/console/views/application-integrations-webhooks/connect.js @@ -51,6 +51,7 @@ const selector = [ 'name', 'service_data', 'uplink_message', + 'uplink_normalized', ] const mapStateToProps = state => ({ diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index 9cf785e3ad..934b7b2038 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -189,6 +189,7 @@ export default defineMessages({ eventLocationSolvedDesc: 'An integration succeeded locating the end device', eventServiceDataDesc: 'An integration emits an event', eventUplinkMessageDesc: 'An uplink message is received by the application', + eventUplinkNormalizedDesc: 'A normalized uplink payload', eventsCannotShow: 'Cannot show events', expiry: 'Expiry', exportJson: 'Export as JSON', @@ -295,6 +296,8 @@ export default defineMessages({ noLocation: 'No location information available', noMatch: 'No items found', none: 'None', + normalizedPayloadAir: 'Air', + normalizedPayloadWind: 'Wind', notAvailable: 'n/a', notLinked: 'Not linked', notSet: 'Not set', @@ -416,6 +419,7 @@ export default defineMessages({ uplink: 'Uplink', uplinkFrameCount: 'Uplink frame count', uplinkMessage: 'Uplink message', + uplinkNormalized: 'Normalized uplink', uplinksReceived: 'Uplinks received', unexposed: 'Unexposed', used: '{currentValue}/{maxValue} used', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 834c4fc18a..dffbf31a5f 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -1219,6 +1219,7 @@ "lib.shared-messages.eventLocationSolvedDesc": "An integration succeeded locating the end device", "lib.shared-messages.eventServiceDataDesc": "An integration emits an event", "lib.shared-messages.eventUplinkMessageDesc": "An uplink message is received by the application", + "lib.shared-messages.eventUplinkNormalizedDesc": "A normalized uplink payload", "lib.shared-messages.eventsCannotShow": "Cannot show events", "lib.shared-messages.expiry": "Expiry", "lib.shared-messages.exportJson": "Export as JSON", @@ -1319,6 +1320,8 @@ "lib.shared-messages.noLocation": "No location information available", "lib.shared-messages.noMatch": "No items found", "lib.shared-messages.none": "None", + "lib.shared-messages.normalizedPayloadAir": "Air", + "lib.shared-messages.normalizedPayloadWind": "Wind", "lib.shared-messages.notAvailable": "n/a", "lib.shared-messages.notLinked": "Not linked", "lib.shared-messages.notSet": "Not set", @@ -1436,6 +1439,7 @@ "lib.shared-messages.uplink": "Uplink", "lib.shared-messages.uplinkFrameCount": "Uplink frame count", "lib.shared-messages.uplinkMessage": "Uplink message", + "lib.shared-messages.uplinkNormalized": "Normalized uplink", "lib.shared-messages.uplinksReceived": "Uplinks received", "lib.shared-messages.unexposed": "Unexposed", "lib.shared-messages.used": "{currentValue}/{maxValue} used", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index c7241aab95..489283c134 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -1791,6 +1791,7 @@ "event:as.up.join.forward": "ジョイン許可メッセージを転送", "event:as.up.join.receive": "ジョイン許可メッセージを受信", "event:as.up.location.forward": "ロケーション解明メッセージの転送", + "event:as.up.normalized.forward": "正規化されたアップリンクデータメッセージの転送", "event:as.up.service.forward": "サービスデータメッセージの転送", "event:client.collaborator.delete": "クライアントコラボレータの削除", "event:client.collaborator.update": "クライアントコラボレータの更新", diff --git a/sdk/js/generated/api-definition.json b/sdk/js/generated/api-definition.json index 577a75bc8a..b7275cd545 100644 --- a/sdk/js/generated/api-definition.json +++ b/sdk/js/generated/api-definition.json @@ -810,6 +810,8 @@ "up.uplink_message.network_ids.net_id", "up.uplink_message.network_ids.tenant_address", "up.uplink_message.network_ids.tenant_id", + "up.uplink_message.normalized_payload", + "up.uplink_message.normalized_payload_warnings", "up.uplink_message.received_at", "up.uplink_message.rx_metadata", "up.uplink_message.session_key_id", @@ -844,6 +846,55 @@ "up.uplink_message.version_ids.serial_number", "up.uplink_message.version_ids.vendor_id", "up.uplink_message.version_ids.vendor_profile_id", + "up.uplink_normalized", + "up.uplink_normalized.confirmed", + "up.uplink_normalized.consumed_airtime", + "up.uplink_normalized.f_cnt", + "up.uplink_normalized.f_port", + "up.uplink_normalized.frm_payload", + "up.uplink_normalized.locations", + "up.uplink_normalized.network_ids", + "up.uplink_normalized.network_ids.cluster_address", + "up.uplink_normalized.network_ids.cluster_id", + "up.uplink_normalized.network_ids.net_id", + "up.uplink_normalized.network_ids.tenant_address", + "up.uplink_normalized.network_ids.tenant_id", + "up.uplink_normalized.normalized_payload", + "up.uplink_normalized.normalized_payload_warnings", + "up.uplink_normalized.received_at", + "up.uplink_normalized.rx_metadata", + "up.uplink_normalized.session_key_id", + "up.uplink_normalized.settings", + "up.uplink_normalized.settings.coding_rate", + "up.uplink_normalized.settings.concentrator_timestamp", + "up.uplink_normalized.settings.data_rate", + "up.uplink_normalized.settings.data_rate.modulation", + "up.uplink_normalized.settings.data_rate.modulation.fsk", + "up.uplink_normalized.settings.data_rate.modulation.fsk.bit_rate", + "up.uplink_normalized.settings.data_rate.modulation.lora", + "up.uplink_normalized.settings.data_rate.modulation.lora.bandwidth", + "up.uplink_normalized.settings.data_rate.modulation.lora.spreading_factor", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss.coding_rate", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss.modulation_type", + "up.uplink_normalized.settings.data_rate.modulation.lrfhss.operating_channel_width", + "up.uplink_normalized.settings.downlink", + "up.uplink_normalized.settings.downlink.antenna_index", + "up.uplink_normalized.settings.downlink.invert_polarization", + "up.uplink_normalized.settings.downlink.tx_power", + "up.uplink_normalized.settings.enable_crc", + "up.uplink_normalized.settings.frequency", + "up.uplink_normalized.settings.time", + "up.uplink_normalized.settings.timestamp", + "up.uplink_normalized.version_ids", + "up.uplink_normalized.version_ids.band_id", + "up.uplink_normalized.version_ids.brand_id", + "up.uplink_normalized.version_ids.firmware_version", + "up.uplink_normalized.version_ids.hardware_version", + "up.uplink_normalized.version_ids.model_id", + "up.uplink_normalized.version_ids.serial_number", + "up.uplink_normalized.version_ids.vendor_id", + "up.uplink_normalized.version_ids.vendor_profile_id", "up.uplink_message.app_s_key", "up.uplink_message.app_s_key.encrypted_key", "up.uplink_message.app_s_key.kek_label", @@ -863,6 +914,8 @@ "up.uplink_message.network_ids.net_id", "up.uplink_message.network_ids.tenant_address", "up.uplink_message.network_ids.tenant_id", + "up.uplink_message.normalized_payload", + "up.uplink_message.normalized_payload_warnings", "up.uplink_message.received_at", "up.uplink_message.rx_metadata", "up.uplink_message.session_key_id", @@ -1300,7 +1353,9 @@ "service_data.topic", "updated_at", "uplink_message", - "uplink_message.topic" + "uplink_message.topic", + "uplink_normalized", + "uplink_normalized.topic" ] }, "List": { @@ -1375,7 +1430,9 @@ "service_data.topic", "updated_at", "uplink_message", - "uplink_message.topic" + "uplink_message.topic", + "uplink_normalized", + "uplink_normalized.topic" ] }, "Set": { @@ -1460,7 +1517,9 @@ "service_data.topic", "updated_at", "uplink_message", - "uplink_message.topic" + "uplink_message.topic", + "uplink_normalized", + "uplink_normalized.topic" ] }, "Delete": { @@ -1532,7 +1591,9 @@ "service_data", "service_data.path", "uplink_message", - "uplink_message.path" + "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path" ] }, "ListTemplates": { @@ -1577,7 +1638,9 @@ "service_data", "service_data.path", "uplink_message", - "uplink_message.path" + "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path" ] }, "Get": { @@ -1646,7 +1709,9 @@ "template_ids.template_id", "updated_at", "uplink_message", - "uplink_message.path" + "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path" ] }, "List": { @@ -1714,7 +1779,9 @@ "template_ids.template_id", "updated_at", "uplink_message", - "uplink_message.path" + "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path" ] }, "Set": { @@ -1792,7 +1859,9 @@ "template_ids.template_id", "updated_at", "uplink_message", - "uplink_message.path" + "uplink_message.path", + "uplink_normalized", + "uplink_normalized.path" ] }, "Delete": { diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index 1805613ac6..19e09639c6 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -4356,6 +4356,18 @@ "oneofdecl": "", "defaultValue": "" }, + { + "name": "uplink_normalized", + "description": "", + "label": "", + "type": "Message", + "longType": "ApplicationPubSub.Message", + "fullType": "ttn.lorawan.v3.ApplicationPubSub.Message", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, { "name": "join_accept", "description": "", @@ -5656,6 +5668,18 @@ "oneofdecl": "", "defaultValue": "" }, + { + "name": "uplink_normalized", + "description": "", + "label": "", + "type": "Message", + "longType": "ApplicationWebhook.Message", + "fullType": "ttn.lorawan.v3.ApplicationWebhook.Message", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, { "name": "join_accept", "description": "", @@ -6351,6 +6375,18 @@ "oneofdecl": "", "defaultValue": "" }, + { + "name": "uplink_normalized", + "description": "", + "label": "", + "type": "Message", + "longType": "ApplicationWebhookTemplate.Message", + "fullType": "ttn.lorawan.v3.ApplicationWebhookTemplate.Message", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, { "name": "join_accept", "description": "", @@ -31500,7 +31536,15 @@ "ismap": false, "isoneof": false, "oneofdecl": "", - "defaultValue": "" + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "timestamp.required", + "value": true + } + ] + } } ] }, @@ -31726,6 +31770,18 @@ "oneofdecl": "up", "defaultValue": "" }, + { + "name": "uplink_normalized", + "description": "", + "label": "", + "type": "ApplicationUplinkNormalized", + "longType": "ApplicationUplinkNormalized", + "fullType": "ttn.lorawan.v3.ApplicationUplinkNormalized", + "ismap": false, + "isoneof": true, + "oneofdecl": "up", + "defaultValue": "" + }, { "name": "join_accept", "description": "", @@ -31880,7 +31936,7 @@ }, { "name": "f_port", - "description": "", + "description": "LoRaWAN FPort of the uplink message.", "label": "", "type": "uint32", "longType": "uint32", @@ -31906,7 +31962,7 @@ }, { "name": "f_cnt", - "description": "", + "description": "LoRaWAN FCntUp of the uplink message.", "label": "", "type": "uint32", "longType": "uint32", @@ -31952,6 +32008,30 @@ "oneofdecl": "", "defaultValue": "" }, + { + "name": "normalized_payload", + "description": "The normalized frame payload of the uplink message.\nThis field is set by the message processor that is configured for the end device (see formatters) or application (see default_formatters).\nIf the message processor is a custom script, there is no uplink normalizer script and the decoded output is valid\nnormalized payload, this field contains the decoded payload.", + "label": "repeated", + "type": "Struct", + "longType": "google.protobuf.Struct", + "fullType": "google.protobuf.Struct", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "normalized_payload_warnings", + "description": "Warnings generated by the message processor while normalizing the decoded payload.", + "label": "repeated", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, { "name": "rx_metadata", "description": "A list of metadata for each antenna of each gateway that received this message.", @@ -31966,7 +32046,7 @@ }, { "name": "settings", - "description": "Settings for the transmission.", + "description": "Transmission settings used by the end device.", "label": "", "type": "TxSettings", "longType": "TxSettings", @@ -32022,7 +32102,7 @@ }, { "name": "confirmed", - "description": "", + "description": "Indicates whether the end device used confirmed data uplink.", "label": "", "type": "bool", "longType": "bool", @@ -32034,7 +32114,7 @@ }, { "name": "consumed_airtime", - "description": "Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the RawPayload size and the transmission settings.", + "description": "Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings.", "label": "", "type": "Duration", "longType": "google.protobuf.Duration", @@ -32118,6 +32198,272 @@ } ] }, + { + "name": "ApplicationUplinkNormalized", + "longName": "ApplicationUplinkNormalized", + "fullName": "ttn.lorawan.v3.ApplicationUplinkNormalized", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "session_key_id", + "description": "Join Server issued identifier for the session keys used by this uplink.", + "label": "", + "type": "bytes", + "longType": "bytes", + "fullType": "bytes", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "bytes.max_len", + "value": 2048 + } + ] + } + }, + { + "name": "f_port", + "description": "LoRaWAN FPort of the uplink message.", + "label": "", + "type": "uint32", + "longType": "uint32", + "fullType": "uint32", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "uint32.lte", + "value": 255 + }, + { + "name": "uint32.gte", + "value": 1 + }, + { + "name": "uint32.not_in", + "value": [ + 224 + ] + } + ] + } + }, + { + "name": "f_cnt", + "description": "LoRaWAN FCntUp of the uplink message.", + "label": "", + "type": "uint32", + "longType": "uint32", + "fullType": "uint32", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "frm_payload", + "description": "The frame payload of the uplink message.\nThis field is always decrypted with AppSKey.", + "label": "", + "type": "bytes", + "longType": "bytes", + "fullType": "bytes", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "normalized_payload", + "description": "The normalized frame payload of the uplink message.\nThis field is set for each item in normalized_payload in the corresponding ApplicationUplink message.", + "label": "", + "type": "Struct", + "longType": "google.protobuf.Struct", + "fullType": "google.protobuf.Struct", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "message.required", + "value": true + } + ] + } + }, + { + "name": "normalized_payload_warnings", + "description": "This field is set to normalized_payload_warnings in the corresponding ApplicationUplink message.", + "label": "repeated", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "rx_metadata", + "description": "A list of metadata for each antenna of each gateway that received this message.", + "label": "repeated", + "type": "RxMetadata", + "longType": "RxMetadata", + "fullType": "ttn.lorawan.v3.RxMetadata", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "settings", + "description": "Transmission settings used by the end device.", + "label": "", + "type": "TxSettings", + "longType": "TxSettings", + "fullType": "ttn.lorawan.v3.TxSettings", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "message.required", + "value": true + } + ] + } + }, + { + "name": "received_at", + "description": "Server time when the Network Server received the message.", + "label": "", + "type": "Timestamp", + "longType": "google.protobuf.Timestamp", + "fullType": "google.protobuf.Timestamp", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "", + "options": { + "validate.rules": [ + { + "name": "timestamp.required", + "value": true + } + ] + } + }, + { + "name": "confirmed", + "description": "Indicates whether the end device used confirmed data uplink.", + "label": "", + "type": "bool", + "longType": "bool", + "fullType": "bool", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "consumed_airtime", + "description": "Consumed airtime for the transmission of the uplink message. Calculated by Network Server using the raw payload size and the transmission settings.", + "label": "", + "type": "Duration", + "longType": "google.protobuf.Duration", + "fullType": "google.protobuf.Duration", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "locations", + "description": "End device location metadata, set by the Application Server while handling the message.", + "label": "repeated", + "type": "LocationsEntry", + "longType": "ApplicationUplinkNormalized.LocationsEntry", + "fullType": "ttn.lorawan.v3.ApplicationUplinkNormalized.LocationsEntry", + "ismap": true, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "version_ids", + "description": "End device version identifiers, set by the Application Server while handling the message.", + "label": "", + "type": "EndDeviceVersionIdentifiers", + "longType": "EndDeviceVersionIdentifiers", + "fullType": "ttn.lorawan.v3.EndDeviceVersionIdentifiers", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "network_ids", + "description": "Network identifiers, set by the Network Server that handles the message.", + "label": "", + "type": "NetworkIdentifiers", + "longType": "NetworkIdentifiers", + "fullType": "ttn.lorawan.v3.NetworkIdentifiers", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, + { + "name": "LocationsEntry", + "longName": "ApplicationUplinkNormalized.LocationsEntry", + "fullName": "ttn.lorawan.v3.ApplicationUplinkNormalized.LocationsEntry", + "description": "", + "hasExtensions": false, + "hasFields": true, + "hasOneofs": false, + "extensions": [], + "fields": [ + { + "name": "key", + "description": "", + "label": "", + "type": "string", + "longType": "string", + "fullType": "string", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + }, + { + "name": "value", + "description": "", + "label": "", + "type": "Location", + "longType": "Location", + "fullType": "ttn.lorawan.v3.Location", + "ismap": false, + "isoneof": false, + "oneofdecl": "", + "defaultValue": "" + } + ] + }, { "name": "DownlinkMessage", "longName": "DownlinkMessage", diff --git a/tools/go.mod b/tools/go.mod index 7f50e6c012..aed96fcc3b 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -94,8 +94,8 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/disintegration/imaging v1.6.2 // indirect - github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect - github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/eclipse/paho.mqtt.golang v1.3.5 // indirect github.com/envoyproxy/protoc-gen-validate v0.6.3 // indirect diff --git a/tools/go.sum b/tools/go.sum index 6603410892..cbb6393308 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -199,11 +199,14 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f h1:ztRywKO1rqqS8li0TDcnwi9AGsqAH0ky9NaND69/Ccc= -github.com/dop251/goja v0.0.0-20220214123719-b09a6bfa842f/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6 h1:xHdUVG+c8SWJnct16Z3QJOVlaYo3OwoJyamo6kR6OL0= +github.com/dop251/goja v0.0.0-20220815083517-0c74f9139fd6/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= @@ -534,6 +537,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -702,6 +706,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=