diff --git a/api/api.md b/api/api.md index 6f343fa966..87d1faa951 100644 --- a/api/api.md +++ b/api/api.md @@ -5902,7 +5902,7 @@ Only the components for which the keys were meant, will have the key-encryption- | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | | `modulation_type` | [`uint32`](#uint32) | | | -| `operating_channel_width` | [`uint32`](#uint32) | | Operating Channel Width (kHz). | +| `operating_channel_width` | [`uint32`](#uint32) | | Operating Channel Width (Hz). | | `coding_rate` | [`string`](#string) | | | ### Message `LoRaDataRate` diff --git a/api/api.swagger.json b/api/api.swagger.json index f779d44457..a5a9359d33 100644 --- a/api/api.swagger.json +++ b/api/api.swagger.json @@ -22117,7 +22117,7 @@ "operating_channel_width": { "type": "integer", "format": "int64", - "description": "Operating Channel Width (kHz)." + "description": "Operating Channel Width (Hz)." }, "coding_rate": { "type": "string" diff --git a/api/lorawan.proto b/api/lorawan.proto index 5c48187029..2f5f3b9d94 100644 --- a/api/lorawan.proto +++ b/api/lorawan.proto @@ -410,7 +410,7 @@ message FSKDataRate { message LRFHSSDataRate { option (thethings.flags.message) = { select: true, set: false }; uint32 modulation_type = 1; - // Operating Channel Width (kHz). + // Operating Channel Width (Hz). uint32 operating_channel_width = 2; string coding_rate = 3; } diff --git a/config/messages.json b/config/messages.json index e8fb07c9ba..74311d334b 100644 --- a/config/messages.json +++ b/config/messages.json @@ -4562,6 +4562,15 @@ "file": "firewall.go" } }, + "error:pkg/gatewayserver/io/udp:packet_type": { + "translations": { + "en": "invalid packet type" + }, + "description": { + "package": "pkg/gatewayserver/io/udp", + "file": "udp.go" + } + }, "error:pkg/gatewayserver/io/udp:rate_exceeded": { "translations": { "en": "gateway traffic exceeded allowed rate" diff --git a/pkg/gatewayserver/io/udp/observability.go b/pkg/gatewayserver/io/udp/observability.go new file mode 100644 index 0000000000..68f005e69f --- /dev/null +++ b/pkg/gatewayserver/io/udp/observability.go @@ -0,0 +1,113 @@ +// 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 udp + +import ( + "context" + "encoding/json" + + "github.com/prometheus/client_golang/prometheus" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/metrics" + encoding "go.thethings.network/lorawan-stack/v3/pkg/ttnpb/udp" +) + +const subsystem = "gs_io_udp" + +var udpMetrics = &messageMetrics{ + messageReceived: prometheus.NewCounter( + prometheus.CounterOpts{ + Subsystem: subsystem, + Name: "message_received_total", + Help: "Total number of received UDP messages", + }, + ), + messageForwarded: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: subsystem, + Name: "message_forwarded_total", + Help: "Total number of forwarded UDP messages", + }, + []string{"type"}, + ), + messageDropped: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: subsystem, + Name: "message_dropped_total", + Help: "Total number of dropped UDP messages", + }, + []string{"error"}, + ), + + unmarshalTypeErrors: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: subsystem, + Name: "unmarshal_type_errors_total", + Help: "Total number of unmarshal type errors", + }, + []string{"value", "field"}, + ), +} + +func init() { + metrics.MustRegister(udpMetrics) +} + +type messageMetrics struct { + messageReceived prometheus.Counter + messageForwarded *prometheus.CounterVec + messageDropped *prometheus.CounterVec + + unmarshalTypeErrors *prometheus.CounterVec +} + +// Describe implements prometheus.Collector. +func (m messageMetrics) Describe(ch chan<- *prometheus.Desc) { + m.messageReceived.Describe(ch) + m.messageForwarded.Describe(ch) + m.messageDropped.Describe(ch) + + m.unmarshalTypeErrors.Describe(ch) +} + +// Collect implements prometheus.Collector. +func (m messageMetrics) Collect(ch chan<- prometheus.Metric) { + m.messageReceived.Collect(ch) + m.messageForwarded.Collect(ch) + m.messageDropped.Collect(ch) + + m.unmarshalTypeErrors.Collect(ch) +} + +func registerMessageReceived(_ context.Context) { + udpMetrics.messageReceived.Inc() +} + +func registerMessageForwarded(_ context.Context, tp encoding.PacketType) { + udpMetrics.messageForwarded.WithLabelValues(tp.String()).Inc() +} + +func registerMessageDropped(_ context.Context, err error) { + errorLabel := "unknown" + if ttnErr, ok := errors.From(err); ok { + errorLabel = ttnErr.FullName() + } else if jsonErr := (&json.SyntaxError{}); errors.Is(err, jsonErr) { + errorLabel = "encoding/json:syntax" + } else if jsonErr := (&json.UnmarshalTypeError{}); errors.Is(err, jsonErr) { + errorLabel = "encoding/json:unmarshal_type" + udpMetrics.unmarshalTypeErrors.WithLabelValues(jsonErr.Value, jsonErr.Field).Inc() + } + udpMetrics.messageDropped.WithLabelValues(errorLabel).Inc() +} diff --git a/pkg/gatewayserver/io/udp/udp.go b/pkg/gatewayserver/io/udp/udp.go index 019b53ca6a..4472c71dc7 100644 --- a/pkg/gatewayserver/io/udp/udp.go +++ b/pkg/gatewayserver/io/udp/udp.go @@ -33,6 +33,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/unique" "go.thethings.network/lorawan-stack/v3/pkg/workerpool" + "golang.org/x/exp/slices" ) type srv struct { @@ -96,6 +97,8 @@ func Serve(ctx context.Context, server io.Server, conn *net.UDPConn, config Conf return s.read(wp) } +var errPacketType = errors.DefineInvalidArgument("packet_type", "invalid packet type") + func (s *srv) read(wp workerpool.WorkerPool[encoding.Packet]) error { var buf [65507]byte for { @@ -110,15 +113,16 @@ func (s *srv) read(wp workerpool.WorkerPool[encoding.Packet]) error { ctx := log.NewContextWithField(s.ctx, "remote_addr", addr.String()) logger := log.FromContext(ctx) + registerMessageReceived(ctx) if err := ratelimit.Require(s.server.RateLimiter(), ratelimit.GatewayUDPTrafficResource(addr)); err != nil { if ratelimit.Require(s.limitLogs, ratelimit.NewCustomResource(addr.IP.String())) == nil { logger.WithError(err).Warn("Drop packet") } + registerMessageDropped(ctx, err) continue } - packetBuf := make([]byte, n) - copy(packetBuf, buf[:]) + packetBuf := slices.Clone(buf[:n]) packet := encoding.Packet{ GatewayAddr: addr, @@ -126,22 +130,28 @@ func (s *srv) read(wp workerpool.WorkerPool[encoding.Packet]) error { } if err := packet.UnmarshalBinary(packetBuf); err != nil { logger.WithError(err).Debug("Failed to unmarshal packet") + registerMessageDropped(ctx, err) continue } switch packet.PacketType { case encoding.PullData, encoding.PushData, encoding.TxAck: default: logger.WithField("packet_type", packet.PacketType).Debug("Invalid packet type for uplink") + registerMessageDropped(ctx, errPacketType) continue } if packet.GatewayEUI == nil { logger.Debug("No gateway EUI in uplink message") + registerMessageDropped(ctx, errNoEUI) continue } if err := wp.Publish(ctx, packet); err != nil { logger.WithError(err).Warn("UDP packet publishing failed") + registerMessageDropped(ctx, err) + continue } + registerMessageForwarded(ctx, packet.PacketType) } } diff --git a/pkg/gatewayserver/observability.go b/pkg/gatewayserver/observability.go index 41cfd2b523..0df2dab83e 100644 --- a/pkg/gatewayserver/observability.go +++ b/pkg/gatewayserver/observability.go @@ -379,7 +379,7 @@ func registerSuccessDownlink(ctx context.Context, gtw *ttnpb.Gateway, protocol s } func registerFailDownlink(ctx context.Context, gtw *ttnpb.Gateway, txAck *ttnpb.TxAcknowledgment, protocol string) { - events.Publish(evtTxFailureDown.NewWithIdentifiersAndData(ctx, gtw, txAck.Result)) + events.Publish(evtTxFailureDown.NewWithIdentifiersAndData(ctx, gtw, txAck)) gsMetrics.downlinkTxFailed.WithLabelValues(ctx, protocol, txAck.Result.String()).Inc() } diff --git a/pkg/networkserver/mac/adr_param_setup_test.go b/pkg/networkserver/mac/adr_param_setup_test.go index 121a82859b..ebecd81975 100644 --- a/pkg/networkserver/mac/adr_param_setup_test.go +++ b/pkg/networkserver/mac/adr_param_setup_test.go @@ -214,7 +214,7 @@ func TestHandleADRParamSetupAns(t *testing.T) { Events: events.Builders{ EvtReceiveADRParamSetupAnswer, }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_ADR_PARAM_SETUP), }, { Name: "limit 32768, delay 1024", diff --git a/pkg/networkserver/mac/beacon_freq_test.go b/pkg/networkserver/mac/beacon_freq_test.go index 0e5b65affa..27a1ed2213 100644 --- a/pkg/networkserver/mac/beacon_freq_test.go +++ b/pkg/networkserver/mac/beacon_freq_test.go @@ -148,7 +148,7 @@ func TestHandleBeaconFreqAns(t *testing.T) { FrequencyAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_BEACON_FREQ), }, { Name: "nack/no request", @@ -168,7 +168,7 @@ func TestHandleBeaconFreqAns(t *testing.T) { Events: events.Builders{ EvtReceiveBeaconFreqReject.With(events.WithData(&ttnpb.MACCommand_BeaconFreqAns{})), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_BEACON_FREQ), }, { Name: "ack/valid request", diff --git a/pkg/networkserver/mac/dev_status_test.go b/pkg/networkserver/mac/dev_status_test.go index 8fbc892e78..18d30d63d3 100644 --- a/pkg/networkserver/mac/dev_status_test.go +++ b/pkg/networkserver/mac/dev_status_test.go @@ -224,7 +224,7 @@ func TestHandleDevStatusAns(t *testing.T) { Margin: 4, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_DEV_STATUS), }, { Name: "battery 42%/margin 4", diff --git a/pkg/networkserver/mac/dl_channel_test.go b/pkg/networkserver/mac/dl_channel_test.go index 7ffab06dbc..e4c305b964 100644 --- a/pkg/networkserver/mac/dl_channel_test.go +++ b/pkg/networkserver/mac/dl_channel_test.go @@ -384,7 +384,7 @@ func TestHandleDLChannelAns(t *testing.T) { ChannelIndexAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_DL_CHANNEL), }, { Name: "frequency nack/channel index ack/no request", @@ -408,7 +408,7 @@ func TestHandleDLChannelAns(t *testing.T) { ChannelIndexAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_DL_CHANNEL), }, { Name: "frequency nack/channel index nack/valid request/no rejections", diff --git a/pkg/networkserver/mac/duty_cycle_test.go b/pkg/networkserver/mac/duty_cycle_test.go index a7826d1125..d056e80ccb 100644 --- a/pkg/networkserver/mac/duty_cycle_test.go +++ b/pkg/networkserver/mac/duty_cycle_test.go @@ -106,7 +106,7 @@ func TestHandleDutyCycleAns(t *testing.T) { Events: events.Builders{ EvtReceiveDutyCycleAnswer, }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_DUTY_CYCLE), }, { Name: "2048", diff --git a/pkg/networkserver/mac/errors.go b/pkg/networkserver/mac/errors.go index 00f2fecdcd..0aea33768b 100644 --- a/pkg/networkserver/mac/errors.go +++ b/pkg/networkserver/mac/errors.go @@ -19,6 +19,8 @@ import ( ) var ( - ErrRequestNotFound = errors.DefineInvalidArgument("request_not_found", "MAC response received, but corresponding request not found") - ErrNoPayload = errors.DefineInvalidArgument("no_payload", "no message payload specified") + ErrRequestNotFound = errors.DefineInvalidArgument( + "request_not_found", "MAC response received, but corresponding request not found", "cid", + ) + ErrNoPayload = errors.DefineInvalidArgument("no_payload", "no message payload specified") ) diff --git a/pkg/networkserver/mac/link_adr.go b/pkg/networkserver/mac/link_adr.go index f8b85d3d12..42efc7aa52 100644 --- a/pkg/networkserver/mac/link_adr.go +++ b/pkg/networkserver/mac/link_adr.go @@ -141,11 +141,6 @@ func generateLinkADRReq(ctx context.Context, dev *ttnpb.EndDevice, phy *band.Ban "desired_adr_data_rate_index", dev.MacState.DesiredParameters.AdrDataRateIndex, } switch { - case dev.MacState.DesiredParameters.AdrDataRateIndex > phy.MaxADRDataRateIndex: - return linkADRReqParameters{}, false, internal.ErrCorruptedMACState. - WithAttributes(append(attributes, - "phy_max_adr_data_rate_index", phy.MaxADRDataRateIndex, - )...) case dev.MacState.DesiredParameters.AdrDataRateIndex < minDataRateIndex: return linkADRReqParameters{}, false, internal.ErrCorruptedMACState. WithAttributes(append(attributes, diff --git a/pkg/networkserver/mac/link_adr_test.go b/pkg/networkserver/mac/link_adr_test.go index 94f06a52ba..ff0488c7bd 100644 --- a/pkg/networkserver/mac/link_adr_test.go +++ b/pkg/networkserver/mac/link_adr_test.go @@ -481,7 +481,7 @@ func TestHandleLinkADRAns(t *testing.T) { TxPowerIndexAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_LINK_ADR), }, { Name: "1 request/all ack", diff --git a/pkg/networkserver/mac/mac.go b/pkg/networkserver/mac/mac.go index 7cdbba6785..51f422f3da 100644 --- a/pkg/networkserver/mac/mac.go +++ b/pkg/networkserver/mac/mac.go @@ -1,4 +1,4 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. +// 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. @@ -69,7 +69,7 @@ func handleMACResponse( if allowMissing { return cmds, nil } - return cmds, ErrRequestNotFound.New() + return cmds, ErrRequestNotFound.WithAttributes("cid", cid) } // handleMACResponse searches for first MAC command block in cmds with CID equal to cid and calls f for each found value as argument. @@ -79,7 +79,8 @@ func handleMACResponse( func handleMACResponseBlock( cid ttnpb.MACCommandIdentifier, allowMissing bool, - f func(*ttnpb.MACCommand) error, cmds ...*ttnpb.MACCommand, + f func(*ttnpb.MACCommand) error, + cmds ...*ttnpb.MACCommand, ) ([]*ttnpb.MACCommand, error) { first := -1 last := -1 @@ -90,12 +91,14 @@ outer: switch { case first >= 0 && cmd.Cid != cid: + last-- break outer case first < 0 && cmd.Cid != cid: continue case first < 0: first = i } + if err := f(cmd); err != nil { return cmds, err } @@ -104,7 +107,7 @@ outer: case first < 0 && allowMissing: return cmds, nil case first < 0 && !allowMissing: - return cmds, ErrRequestNotFound.New() + return cmds, ErrRequestNotFound.WithAttributes("cid", cid) default: return append(cmds[:first], cmds[last+1:]...), nil } diff --git a/pkg/networkserver/mac/mac_internal_test.go b/pkg/networkserver/mac/mac_internal_test.go new file mode 100644 index 0000000000..4f52ef2009 --- /dev/null +++ b/pkg/networkserver/mac/mac_internal_test.go @@ -0,0 +1,408 @@ +// 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 mac + +import ( + "testing" + + "github.com/smartystreets/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" +) + +func TestHandleMACResponseBlock(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + Name string + + CID ttnpb.MACCommandIdentifier + AllowMissing bool + F func(*assertions.Assertion, *ttnpb.MACCommand) error + Commands []*ttnpb.MACCommand + + Result []*ttnpb.MACCommand + AssertError func(*assertions.Assertion, error) + }{ + { + Name: "LinkADRReq", + + CID: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + AllowMissing: false, + F: func(a *assertions.Assertion, cmd *ttnpb.MACCommand) error { + a.So(cmd, should.Resemble, &ttnpb.MACCommand{ + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }) + return nil + }, + Commands: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + }, + + Result: []*ttnpb.MACCommand{}, + }, + { + Name: "LinkADRReq+LinkADRReq", + + CID: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + AllowMissing: false, + F: func(a *assertions.Assertion, cmd *ttnpb.MACCommand) error { + a.So(cmd, should.Resemble, &ttnpb.MACCommand{ + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }) + return nil + }, + Commands: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + }, + + Result: []*ttnpb.MACCommand{}, + }, + { + Name: "LinkADRReq+DlChannelReq", + + CID: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + AllowMissing: false, + F: func(a *assertions.Assertion, cmd *ttnpb.MACCommand) error { + a.So(cmd, should.Resemble, &ttnpb.MACCommand{ + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }) + return nil + }, + Commands: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + + Result: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + }, + { + Name: "DlChannelReq+LinkADRReq", + + CID: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + AllowMissing: false, + F: func(a *assertions.Assertion, cmd *ttnpb.MACCommand) error { + a.So(cmd, should.Resemble, &ttnpb.MACCommand{ + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }) + return nil + }, + Commands: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + }, + + Result: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + }, + { + Name: "LinkADRReq+LinkADRReq+DlChannelReq", + + CID: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + AllowMissing: false, + F: func(a *assertions.Assertion, cmd *ttnpb.MACCommand) error { + a.So(cmd, should.Resemble, &ttnpb.MACCommand{ + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }) + return nil + }, + Commands: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + + Result: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + }, + { + Name: "DlChannelReq+LinkADRReq+LinkADRReq+DlChannelReq", + + CID: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + AllowMissing: false, + F: func(a *assertions.Assertion, cmd *ttnpb.MACCommand) error { + a.So(cmd, should.Resemble, &ttnpb.MACCommand{ + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }) + return nil + }, + Commands: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_LINK_ADR, + Payload: &ttnpb.MACCommand_LinkAdrReq{ + LinkAdrReq: &ttnpb.MACCommand_LinkADRReq{ + DataRateIndex: 1, + TxPowerIndex: 2, + ChannelMask: []bool{false, true, false}, + ChannelMaskControl: 3, + NbTrans: 4, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + + Result: []*ttnpb.MACCommand{ + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + { + Cid: ttnpb.MACCommandIdentifier_CID_DL_CHANNEL, + Payload: &ttnpb.MACCommand_DlChannelReq{ + DlChannelReq: &ttnpb.MACCommand_DLChannelReq{ + ChannelIndex: 1, + Frequency: 2, + }, + }, + }, + }, + }, + } { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + a := assertions.New(t) + rest, err := handleMACResponseBlock(tc.CID, tc.AllowMissing, func(cmd *ttnpb.MACCommand) error { + return tc.F(a, cmd) + }, tc.Commands...) + if tc.AssertError == nil { + a.So(err, should.BeNil) + a.So(rest, should.Resemble, tc.Result) + } else { + tc.AssertError(a, err) + } + }) + } +} diff --git a/pkg/networkserver/mac/new_channel_test.go b/pkg/networkserver/mac/new_channel_test.go index 91a6299a77..d39b581ba1 100644 --- a/pkg/networkserver/mac/new_channel_test.go +++ b/pkg/networkserver/mac/new_channel_test.go @@ -449,7 +449,7 @@ func TestHandleNewChannelAns(t *testing.T) { DataRateAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_NEW_CHANNEL), }, { Name: "frequency nack/data rate ack/no rejections", diff --git a/pkg/networkserver/mac/ping_slot_channel_test.go b/pkg/networkserver/mac/ping_slot_channel_test.go index 34b62fb491..69ca934efe 100644 --- a/pkg/networkserver/mac/ping_slot_channel_test.go +++ b/pkg/networkserver/mac/ping_slot_channel_test.go @@ -150,7 +150,7 @@ func TestHandlePingSlotChannelAns(t *testing.T) { DataRateIndexAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_PING_SLOT_CHANNEL), }, { Name: "both ack", diff --git a/pkg/networkserver/mac/rejoin_param_setup_test.go b/pkg/networkserver/mac/rejoin_param_setup_test.go index ddda6ee90b..89ba02561d 100644 --- a/pkg/networkserver/mac/rejoin_param_setup_test.go +++ b/pkg/networkserver/mac/rejoin_param_setup_test.go @@ -163,7 +163,7 @@ func TestHandleRejoinParamSetupAns(t *testing.T) { MaxTimeExponentAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_REJOIN_PARAM_SETUP), }, { Name: "ack", diff --git a/pkg/networkserver/mac/rx_param_setup_test.go b/pkg/networkserver/mac/rx_param_setup_test.go index b2151a468a..7d49af19bd 100644 --- a/pkg/networkserver/mac/rx_param_setup_test.go +++ b/pkg/networkserver/mac/rx_param_setup_test.go @@ -184,7 +184,7 @@ func TestHandleRxParamSetupAns(t *testing.T) { Rx2FrequencyAck: true, })), }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_RX_PARAM_SETUP), }, { Name: "all ack", diff --git a/pkg/networkserver/mac/rx_timing_setup_test.go b/pkg/networkserver/mac/rx_timing_setup_test.go index 227c9720e3..c8f5303fae 100644 --- a/pkg/networkserver/mac/rx_timing_setup_test.go +++ b/pkg/networkserver/mac/rx_timing_setup_test.go @@ -122,7 +122,7 @@ func TestHandleRxTimingSetupAns(t *testing.T) { Events: events.Builders{ EvtReceiveRxTimingSetupAnswer, }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_RX_TIMING_SETUP), }, { Name: "42", diff --git a/pkg/networkserver/mac/tx_param_setup_test.go b/pkg/networkserver/mac/tx_param_setup_test.go index 5177493152..47a6ac2a8f 100644 --- a/pkg/networkserver/mac/tx_param_setup_test.go +++ b/pkg/networkserver/mac/tx_param_setup_test.go @@ -378,7 +378,7 @@ func TestHandleTxParamSetupAns(t *testing.T) { Events: events.Builders{ EvtReceiveTxParamSetupAnswer, }, - Error: ErrRequestNotFound, + Error: ErrRequestNotFound.WithAttributes("cid", ttnpb.MACCommandIdentifier_CID_TX_PARAM_SETUP), }, { Name: "EIRP 26, dwell time both", diff --git a/pkg/packetbrokeragent/translation.go b/pkg/packetbrokeragent/translation.go index 19d0236e6d..462d464bdc 100644 --- a/pkg/packetbrokeragent/translation.go +++ b/pkg/packetbrokeragent/translation.go @@ -77,18 +77,16 @@ func fromPBDataRate(dataRate *packetbroker.DataRate) (dr *ttnpb.DataRate, coding }, }, }, "", true - // TODO: Support LR-FHSS (https://github.com/TheThingsNetwork/lorawan-stack/issues/3806) - // TODO: Set coding rate from data rate (https://github.com/TheThingsNetwork/lorawan-stack/issues/4466) - // case *packetbroker.DataRate_Lrfhss: - // return &ttnpb.DataRate{ - // Modulation: &ttnpb.DataRate_Lrfhss{ - // Lrfhss: &ttnpb.LRFHSSDataRate{ - // ModulationType: mod.Lrfhss.ModulationType, - // OperatingChannelWidth: mod.Lrfhss.OperatingChannelWidth, - // CodingRate: mod.Lrfhss.CodingRate, - // }, - // }, - // }, mod.Lrfhss.CodingRate, true + case *packetbroker.DataRate_Lrfhss: + return &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: mod.Lrfhss.ModulationType, + OperatingChannelWidth: mod.Lrfhss.OperatingChannelWidth, + CodingRate: mod.Lrfhss.CodingRate, + }, + }, + }, mod.Lrfhss.CodingRate, true default: return nil, "", false } @@ -118,18 +116,16 @@ func toPBDataRate(dataRate *ttnpb.DataRate, codingRate string) (*packetbroker.Da }, }, }, true - // TODO: Support LR-FHSS (https://github.com/TheThingsNetwork/lorawan-stack/issues/3806) - // TODO: Get coding rate from data rate (https://github.com/TheThingsNetwork/lorawan-stack/issues/4466) - // case *ttnpb.DataRate_Lrfhss: - // return &packetbroker.DataRate{ - // Modulation: &packetbroker.DataRate_Lrfhss{ - // Lrfhss: &packetbroker.LRFHSSDataRate{ - // ModulationType: mod.Lrfhss.ModulationType, - // OperatingChannelWidth: mod.Lrfhss.OperatingChannelWidth, - // CodingRate: mod.Lrfhss.CodingRate, - // }, - // }, - // }, true + case *ttnpb.DataRate_Lrfhss: + return &packetbroker.DataRate{ + Modulation: &packetbroker.DataRate_Lrfhss{ + Lrfhss: &packetbroker.LRFHSSDataRate{ + ModulationType: mod.Lrfhss.ModulationType, + OperatingChannelWidth: mod.Lrfhss.OperatingChannelWidth, + CodingRate: mod.Lrfhss.CodingRate, + }, + }, + }, true default: return nil, false } diff --git a/pkg/ttnpb/lorawan.pb.go b/pkg/ttnpb/lorawan.pb.go index 82530c7f5d..748f029294 100644 --- a/pkg/ttnpb/lorawan.pb.go +++ b/pkg/ttnpb/lorawan.pb.go @@ -1963,7 +1963,7 @@ func (m *FSKDataRate) GetBitRate() uint32 { type LRFHSSDataRate struct { ModulationType uint32 `protobuf:"varint,1,opt,name=modulation_type,json=modulationType,proto3" json:"modulation_type,omitempty"` - // Operating Channel Width (kHz). + // Operating Channel Width (Hz). OperatingChannelWidth uint32 `protobuf:"varint,2,opt,name=operating_channel_width,json=operatingChannelWidth,proto3" json:"operating_channel_width,omitempty"` CodingRate string `protobuf:"bytes,3,opt,name=coding_rate,json=codingRate,proto3" json:"coding_rate,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` diff --git a/pkg/ttnpb/udp/packet_data.go b/pkg/ttnpb/udp/packet_data.go index 70e9485bca..f69998a712 100644 --- a/pkg/ttnpb/udp/packet_data.go +++ b/pkg/ttnpb/udp/packet_data.go @@ -53,6 +53,22 @@ type RxPacket struct { Aesk uint `json:"aesk"` // AES key index used for encrypting fine timestamps (unsigned integer) } +// reduceLRFHSSCodingRate reduces the coding rate fraction returned by the packet forwarder. +// The packet forwarder will render the coding rates used by LR-FHSS in their `4/x` form, even +// though the real coding rates are irreducible fractions. +func reduceLRFHSSCodingRate(cr string) string { + switch cr { + case "4/6": + return "2/3" + case "4/8": + return "1/2" + case "2/6": + return "1/3" + default: + return cr + } +} + // UnmarshalJSON implements json.Unmarshaler. func (p *RxPacket) UnmarshalJSON(data []byte) error { type Alias RxPacket @@ -66,7 +82,7 @@ func (p *RxPacket) UnmarshalJSON(data []byte) error { } switch mod := p.DatR.DataRate.Modulation.(type) { case *ttnpb.DataRate_Lrfhss: - mod.Lrfhss.CodingRate = p.CodR + mod.Lrfhss.CodingRate = reduceLRFHSSCodingRate(p.CodR) } return nil } @@ -120,14 +136,19 @@ type Stat struct { ACKR float64 `json:"ackr"` // Percentage of upstream datagrams that were acknowledged DWNb uint32 `json:"dwnb"` // Number of downlink datagrams received (unsigned integer) TxNb uint32 `json:"txnb"` // Number of packets emitted (unsigned integer) - LMOK *uint32 `json:"lmok,omitempty"` // Number of packets received from link testing mote, with CRC OK (unsigned inteter) + LMOK *uint32 `json:"lmok,omitempty"` // Number of packets received from link testing mote, with CRC OK (unsigned integer) LMST *uint32 `json:"lmst,omitempty"` // Sequence number of the first packet received from link testing mote (unsigned integer) LMNW *uint32 `json:"lmnw,omitempty"` // Sequence number of the last packet received from link testing mote (unsigned integer) LPPS *uint32 `json:"lpps,omitempty"` // Number of lost PPS pulses (unsigned integer) Temp *float32 `json:"temp,omitempty"` // Temperature of the Gateway (signed float) FPGA *uint32 `json:"fpga,omitempty"` // Version of Gateway FPGA (unsigned integer) - DSP *uint32 `json:"dsp,omitempty"` // Version of Gateway DSP software (unsigned interger) + DSP *uint32 `json:"dsp,omitempty"` // Version of Gateway DSP software (unsigned integer) HAL *string `json:"hal,omitempty"` // Version of Gateway driver (format X.X.X) + HVer *struct { + FPGA *uint32 `json:"fpga,omitempty"` // Version of FPGA (unsigned integer) + DSP0 *uint32 `json:"dsp0,omitempty"` // Version of DSP 0 software (unsigned integer) + DSP1 *uint32 `json:"dsp1,omitempty"` // Version of DSP 1 software (unsigned integer) + } `json:"hver,omitempty"` // Gateway hardware versions } // TxError is returned in the TxPacketAck diff --git a/pkg/ttnpb/udp/packet_data_test.go b/pkg/ttnpb/udp/packet_data_test.go index 3a716a17e7..76e3e38a75 100644 --- a/pkg/ttnpb/udp/packet_data_test.go +++ b/pkg/ttnpb/udp/packet_data_test.go @@ -150,7 +150,7 @@ func TestUplinkPacket(t *testing.T) { Modulation: &ttnpb.DataRate_Lrfhss{ Lrfhss: &ttnpb.LRFHSSDataRate{ ModulationType: 0, - OperatingChannelWidth: 123, + OperatingChannelWidth: 123000, CodingRate: "4/7", }, }, diff --git a/pkg/ttnpb/udp/translation.go b/pkg/ttnpb/udp/translation.go index 68bfbfefcf..555fa5774a 100644 --- a/pkg/ttnpb/udp/translation.go +++ b/pkg/ttnpb/udp/translation.go @@ -219,9 +219,11 @@ func convertUplink(rx RxPacket, md UpstreamMetadata) (*ttnpb.UplinkMessage, erro } } - switch up.Settings.DataRate.Modulation.(type) { - case *ttnpb.DataRate_Lora, *ttnpb.DataRate_Lrfhss: + switch mod := up.Settings.DataRate.Modulation.(type) { + case *ttnpb.DataRate_Lora: up.Settings.CodingRate = rx.CodR + case *ttnpb.DataRate_Lrfhss: + up.Settings.CodingRate = mod.Lrfhss.CodingRate } return up, nil @@ -237,6 +239,17 @@ func addVersions(status *ttnpb.GatewayStatus, stat Stat) { if stat.HAL != nil { status.Versions["hal"] = *stat.HAL } + if hver := stat.HVer; hver != nil { + if fpga := hver.FPGA; fpga != nil { + status.Versions["fpga"] = strconv.Itoa(int(*fpga)) + } + if dsp0 := hver.DSP0; dsp0 != nil { + status.Versions["dsp0"] = strconv.Itoa(int(*dsp0)) + } + if dsp1 := hver.DSP1; dsp1 != nil { + status.Versions["dsp1"] = strconv.Itoa(int(*dsp1)) + } + } } func addMetrics(status *ttnpb.GatewayStatus, stat Stat) { @@ -409,13 +422,14 @@ func FromDownlinkMessage(msg *ttnpb.DownlinkMessage) (*TxPacket, error) { } tx.DatR.DataRate = scheduled.DataRate - switch scheduled.DataRate.GetModulation().(type) { + switch mod := scheduled.DataRate.Modulation.(type) { case *ttnpb.DataRate_Lora: tx.CodR = scheduled.CodingRate tx.NCRC = !scheduled.EnableCrc tx.Modu = lora case *ttnpb.DataRate_Fsk: tx.Modu = fsk + tx.FDev = uint16(mod.Fsk.BitRate) / 2 default: return nil, errDataRate.New() } diff --git a/pkg/ttnpb/udp/translation_test.go b/pkg/ttnpb/udp/translation_test.go index 65b2960227..5e4ff9dba8 100644 --- a/pkg/ttnpb/udp/translation_test.go +++ b/pkg/ttnpb/udp/translation_test.go @@ -154,7 +154,17 @@ func TestToGatewayUpLRFHSS(t *testing.T) { Freq: 868.0, Chan: 2, Modu: "LR-FHSS", - DatR: datarate.DR{DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lrfhss{Lrfhss: &ttnpb.LRFHSSDataRate{ModulationType: 0, OperatingChannelWidth: 125}}}}, + DatR: datarate.DR{ + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: 0, + OperatingChannelWidth: 125, + CodingRate: "5/6", + }, + }, + }, + }, CodR: "5/6", Data: "QCkuASaAAAAByFaF53Iu+vzmwQ==", Size: 19, @@ -295,7 +305,7 @@ func TestToGatewayUpRawLRFHSS(t *testing.T) { dr := msg.Settings.DataRate.GetLrfhss() a.So(dr, should.NotBeNil) a.So(dr.ModulationType, should.Equal, 0) - a.So(dr.OperatingChannelWidth, should.Equal, 125) + a.So(dr.OperatingChannelWidth, should.Equal, 125000) a.So(msg.Settings.CodingRate, should.Equal, "2/3") a.So(msg.Settings.Frequency, should.Equal, 868100000) a.So(msg.RxMetadata[0].Timestamp, should.Equal, 368384825) @@ -418,7 +428,7 @@ func TestToGatewayUpRawMultiAntenna(t *testing.T) { }) } -func TestFromDownlinkMessage(t *testing.T) { +func TestFromDownlinkMessageLoRa(t *testing.T) { a := assertions.New(t) msg := &ttnpb.DownlinkMessage{ @@ -444,12 +454,59 @@ func TestFromDownlinkMessage(t *testing.T) { } tx, err := udp.FromDownlinkMessage(msg) a.So(err, should.BeNil) - a.So(tx.DatR, should.Resemble, datarate.DR{DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{Bandwidth: 500000, SpreadingFactor: 10}}}}) + a.So(tx.DatR, should.Resemble, datarate.DR{ + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + Bandwidth: 500000, + SpreadingFactor: 10, + }, + }, + }, + }) a.So(tx.Tmst, should.Equal, 1886440700) a.So(tx.NCRC, should.Equal, true) a.So(tx.Data, should.Equal, "ffOO") } +func TestFromDownlinkMessageFSK(t *testing.T) { + a := assertions.New(t) + + msg := &ttnpb.DownlinkMessage{ + Settings: &ttnpb.DownlinkMessage_Scheduled{ + Scheduled: &ttnpb.TxSettings{ + Frequency: 925700000, + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Fsk{ + Fsk: &ttnpb.FSKDataRate{ + BitRate: 50000, + }, + }, + }, + Downlink: &ttnpb.TxSettings_Downlink{ + TxPower: 20, + }, + Timestamp: 1886440700, + }, + }, + RawPayload: []byte{0x7d, 0xf3, 0x8e}, + } + tx, err := udp.FromDownlinkMessage(msg) + a.So(err, should.BeNil) + a.So(tx.DatR, should.Resemble, datarate.DR{ + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Fsk{ + Fsk: &ttnpb.FSKDataRate{ + BitRate: 50000, + }, + }, + }, + }) + a.So(tx.Tmst, should.Equal, 1886440700) + a.So(tx.FDev, should.Equal, 25000) + a.So(tx.Data, should.Equal, "ffOO") +} + func TestDownlinkRoundtrip(t *testing.T) { a := assertions.New(t) expected := &ttnpb.DownlinkMessage{ @@ -481,17 +538,3 @@ func TestDownlinkRoundtrip(t *testing.T) { a.So(actual, should.HaveEmptyDiff, expected) } - -func TestFromDownlinkMessageDummy(t *testing.T) { - a := assertions.New(t) - - msg := ttnpb.DownlinkMessage{ - Settings: &ttnpb.DownlinkMessage_Scheduled{ - Scheduled: &ttnpb.TxSettings{ - Downlink: &ttnpb.TxSettings_Downlink{}, - }, - }, - } - _, err := udp.FromDownlinkMessage(&msg) - a.So(err, should.NotBeNil) -} diff --git a/pkg/util/datarate/data_rate.go b/pkg/util/datarate/data_rate.go index 6de24d5e63..0a41c2ef8a 100644 --- a/pkg/util/datarate/data_rate.go +++ b/pkg/util/datarate/data_rate.go @@ -95,7 +95,7 @@ func (dr DR) String() string { return fmt.Sprintf("%d", fsk.BitRate) } if lrfhss := dr.GetLrfhss(); lrfhss != nil { - return fmt.Sprintf("M%dCW%d", lrfhss.ModulationType, lrfhss.OperatingChannelWidth) + return fmt.Sprintf("M%dCW%d", lrfhss.ModulationType, lrfhss.OperatingChannelWidth/1000) } return "" } @@ -149,7 +149,7 @@ func ParseLRFHSS(dr string) (DR, error) { Modulation: &ttnpb.DataRate_Lrfhss{ Lrfhss: &ttnpb.LRFHSSDataRate{ ModulationType: uint32(mod), - OperatingChannelWidth: uint32(ocw), + OperatingChannelWidth: uint32(ocw * 1000), }, }, }, diff --git a/pkg/util/datarate/data_rate_test.go b/pkg/util/datarate/data_rate_test.go index bf57cc7a22..7fcf0af305 100644 --- a/pkg/util/datarate/data_rate_test.go +++ b/pkg/util/datarate/data_rate_test.go @@ -27,9 +27,35 @@ func TestDataRate(t *testing.T) { a := assertions.New(t) table := map[string]datarate.DR{ - `"SF7BW125"`: {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 7, Bandwidth: 125000}}}}, - `50000`: {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Fsk{Fsk: &ttnpb.FSKDataRate{BitRate: 50000}}}}, - `"M0CW137"`: {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lrfhss{Lrfhss: &ttnpb.LRFHSSDataRate{ModulationType: 0, OperatingChannelWidth: 137}}}}, + `"SF7BW125"`: { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 7, + Bandwidth: 125000, + }, + }, + }, + }, + `50000`: { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Fsk{ + Fsk: &ttnpb.FSKDataRate{ + BitRate: 50000, + }, + }, + }, + }, + `"M0CW137"`: { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: 0, + OperatingChannelWidth: 137000, + }, + }, + }, + }, } for s, dr := range table { @@ -52,9 +78,36 @@ func TestValidLoRaDataRateParsing(t *testing.T) { a := assertions.New(t) table := map[string]datarate.DR{ - "SF6BW125": {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 6, Bandwidth: 125000}}}}, - "SF9BW500": {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 9, Bandwidth: 500000}}}}, - "SF5BW31.25": {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 5, Bandwidth: 31250}}}}, + "SF6BW125": { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 6, + Bandwidth: 125000, + }, + }, + }, + }, + "SF9BW500": { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 9, + Bandwidth: 500000, + }, + }, + }, + }, + "SF5BW31.25": { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 5, + Bandwidth: 31250, + }, + }, + }, + }, } for dr, expected := range table { actual, err := datarate.ParseLoRa(dr) @@ -80,11 +133,55 @@ func TestStringer(t *testing.T) { a := assertions.New(t) table := map[datarate.DR]string{ - {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 6, Bandwidth: 125000}}}}: "SF6BW125", - {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 9, Bandwidth: 500000}}}}: "SF9BW500", - {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lora{Lora: &ttnpb.LoRaDataRate{SpreadingFactor: 5, Bandwidth: 31250}}}}: "SF5BW31.25", - {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Fsk{Fsk: &ttnpb.FSKDataRate{BitRate: 50000}}}}: "50000", - {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lrfhss{Lrfhss: &ttnpb.LRFHSSDataRate{ModulationType: 0, OperatingChannelWidth: 137}}}}: "M0CW137", + { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 6, + Bandwidth: 125000, + }, + }, + }, + }: "SF6BW125", + { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 9, + Bandwidth: 500000, + }, + }, + }, + }: "SF9BW500", + { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lora{ + Lora: &ttnpb.LoRaDataRate{ + SpreadingFactor: 5, + Bandwidth: 31250, + }, + }, + }, + }: "SF5BW31.25", + { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Fsk{ + Fsk: &ttnpb.FSKDataRate{ + BitRate: 50000, + }, + }, + }, + }: "50000", + { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: 0, + OperatingChannelWidth: 137000, + }, + }, + }, + }: "M0CW137", } for dr, expected := range table { @@ -96,9 +193,36 @@ func TestLRFHSSDataRateParsing(t *testing.T) { a := assertions.New(t) table := map[string]datarate.DR{ - "M0CW137": {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lrfhss{Lrfhss: &ttnpb.LRFHSSDataRate{ModulationType: 0, OperatingChannelWidth: 137}}}}, - "M12CW1375": {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lrfhss{Lrfhss: &ttnpb.LRFHSSDataRate{ModulationType: 12, OperatingChannelWidth: 1375}}}}, - "M1CW1": {DataRate: &ttnpb.DataRate{Modulation: &ttnpb.DataRate_Lrfhss{Lrfhss: &ttnpb.LRFHSSDataRate{ModulationType: 1, OperatingChannelWidth: 1}}}}, + "M0CW137": { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: 0, + OperatingChannelWidth: 137000, + }, + }, + }, + }, + "M12CW1375": { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: 12, + OperatingChannelWidth: 1375000, + }, + }, + }, + }, + "M1CW1": { + DataRate: &ttnpb.DataRate{ + Modulation: &ttnpb.DataRate_Lrfhss{ + Lrfhss: &ttnpb.LRFHSSDataRate{ + ModulationType: 1, + OperatingChannelWidth: 1000, + }, + }, + }, + }, } for dr, expected := range table { actual, err := datarate.ParseLRFHSS(dr) diff --git a/pkg/webui/console/components/events/previews/application-downlink.js b/pkg/webui/console/components/events/previews/application-downlink.js index 7d1c070286..dba5a611f5 100644 --- a/pkg/webui/console/components/events/previews/application-downlink.js +++ b/pkg/webui/console/components/events/previews/application-downlink.js @@ -15,10 +15,12 @@ import React from 'react' 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 ApplicationDownlinkPreview = React.memo(({ event }) => { const { data, identifiers } = event @@ -27,8 +29,17 @@ const ApplicationDownlinkPreview = React.memo(({ event }) => { return ( + {'decoded_payload' in data ? ( + + + {data.frm_payload && ( + + )} + + ) : ( + + )} {data.f_port} - ) }) diff --git a/pkg/webui/console/components/events/previews/application-up.js b/pkg/webui/console/components/events/previews/application-up.js index b7b5b8e663..ce525e8053 100644 --- a/pkg/webui/console/components/events/previews/application-up.js +++ b/pkg/webui/console/components/events/previews/application-up.js @@ -12,20 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Copyright © 2020 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 PropTypes from '@ttn-lw/lib/prop-types' diff --git a/pkg/webui/console/components/events/previews/application-uplink.js b/pkg/webui/console/components/events/previews/application-uplink.js index d90a538a4d..0509c24b72 100644 --- a/pkg/webui/console/components/events/previews/application-uplink.js +++ b/pkg/webui/console/components/events/previews/application-uplink.js @@ -14,11 +14,10 @@ import React from 'react' -import { getGatewayWithHighestSNR } from '@console/components/events/utils' +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 getByPath from '@ttn-lw/lib/get-by-path' import messages from '../messages' @@ -28,22 +27,8 @@ import JSONPayload from './shared/json-payload' const ApplicationUplinkPreview = React.memo(({ event }) => { const { data, identifiers } = event const deviceIds = identifiers[0].device_ids - let snr, rssi - - if ('rx_metadata' in data) { - if (data.rx_metadata.length > 1) { - const gatewayWithHighestSNR = getGatewayWithHighestSNR(data.rx_metadata) - snr = gatewayWithHighestSNR.snr - rssi = gatewayWithHighestSNR.rssi - } else { - snr = data.rx_metadata[0].snr - rssi = data.rx_metadata[0].rssi - } - } - - const bandwidth = getByPath(data, 'settings.data_rate.lora.bandwidth') - const spreadingFactor = getByPath(data, 'settings.data_rate.lora.spreading_factor') - const dataRate = `SF${spreadingFactor}BW${bandwidth / 1000}` + const { snr, rssi } = getSignalInformation(data) + const dataRate = getDataRate(data) return ( @@ -56,7 +41,7 @@ const ApplicationUplinkPreview = React.memo(({ event }) => { )} ) : ( - + )} diff --git a/pkg/webui/console/components/events/previews/downlink-message.js b/pkg/webui/console/components/events/previews/downlink-message.js index ee00b2ca27..cd3f58d14a 100644 --- a/pkg/webui/console/components/events/previews/downlink-message.js +++ b/pkg/webui/console/components/events/previews/downlink-message.js @@ -16,6 +16,8 @@ import React from 'react' import Message from '@ttn-lw/lib/components/message' +import { getDataRate } from '@console/components/events/utils' + import PropTypes from '@ttn-lw/lib/prop-types' import getByPath from '@ttn-lw/lib/get-by-path' @@ -28,9 +30,7 @@ const DownLinkMessagePreview = React.memo(({ event }) => { if ('scheduled' in data) { const txPower = getByPath(data, 'scheduled.downlink.tx_power') - const bandwidth = getByPath(data, 'scheduled.data_rate.lora.bandwidth') - const spreadingFactor = getByPath(data, 'scheduled.data_rate.lora.spreading_factor') - const dataRate = `SF${spreadingFactor}BW${bandwidth / 1000}` + const dataRate = getDataRate(data, 'scheduled') return ( diff --git a/pkg/webui/console/components/events/previews/gateway-uplink-message.js b/pkg/webui/console/components/events/previews/gateway-uplink-message.js index 18b6e35b9b..a7008d3305 100644 --- a/pkg/webui/console/components/events/previews/gateway-uplink-message.js +++ b/pkg/webui/console/components/events/previews/gateway-uplink-message.js @@ -16,7 +16,7 @@ import React from 'react' import Message from '@ttn-lw/lib/components/message' -import { getGatewayWithHighestSNR } from '@console/components/events/utils' +import { getDataRate, getSignalInformation } from '@console/components/events/utils' import PropTypes from '@ttn-lw/lib/prop-types' import getByPath from '@ttn-lw/lib/get-by-path' @@ -46,22 +46,10 @@ const GatewayUplinkMessagePreview = React.memo(({ event }) => { isConfirmed = getByPath(data, 'message.payload.m_hdr.m_type') === 'CONFIRMED_UP' } - if ('rx_metadata' in data.message) { - if (data.message.rx_metadata.length > 1) { - const gatewayWithHighestSNR = getGatewayWithHighestSNR(data.message.rx_metadata) - snr = gatewayWithHighestSNR.snr - rssi = gatewayWithHighestSNR.rssi - } else { - snr = data.message.rx_metadata[0].snr - rssi = data.message.rx_metadata[0].rssi - } - } - - if ('settings' in data.message && 'data_rate' in data.message.settings) { - const bandwidth = getByPath(data, 'message.settings.data_rate.lora.bandwidth') - const spreadingFactor = getByPath(data, 'message.settings.data_rate.lora.spreading_factor') - dataRate = `SF${spreadingFactor}BW${bandwidth / 1000}` - } + const signalInfo = getSignalInformation(data.message) + snr = signalInfo.snr + rssi = signalInfo.rssi + dataRate = getDataRate(data.message) } return ( diff --git a/pkg/webui/console/components/events/utils/index.js b/pkg/webui/console/components/events/utils/index.js index 770b61e396..e625467c6d 100644 --- a/pkg/webui/console/components/events/utils/index.js +++ b/pkg/webui/console/components/events/utils/index.js @@ -91,5 +91,44 @@ export const getPreviewComponentByApplicationUpMessage = message => { return messageType in dataTypeMap ? dataTypeMap[messageType] : DefaultPreview } -export const getGatewayWithHighestSNR = gateways => - gateways.reduce((prev, current) => (prev.snr >= current.snr ? prev : current)) +export const getSignalInformation = data => { + const notFound = { snr: NaN, rssi: NaN } + if (!data) { + return notFound + } + const { rx_metadata } = data + if (!rx_metadata || rx_metadata.length === 0) { + return notFound + } + const { snr, rssi } = rx_metadata.reduce((prev, current) => + prev.snr >= current.snr ? prev : current, + ) + return { snr, rssi } +} + +export const getDataRate = (data, selector = 'settings') => { + if (!data) { + return undefined + } + const { [selector]: container } = data + if (!container) { + return undefined + } + const { data_rate } = container + if (!data_rate) { + return undefined + } + const { lora, fsk, lrfhss } = data_rate + // The encoding below mimics the encoding of the `modu` field of the UDP packet forwarder. + if (lora) { + const { bandwidth, spreading_factor } = lora + return `SF${spreading_factor}BW${bandwidth / 1000}` + } else if (fsk) { + const { bit_rate } = fsk + return `${bit_rate}` + } else if (lrfhss) { + const { modulation_type, operating_channel_width } = lrfhss + return `M${modulation_type ?? 0}CW${operating_channel_width / 1000}` + } + return undefined +} diff --git a/sdk/js/generated/api.json b/sdk/js/generated/api.json index 6106f9ad26..1805613ac6 100644 --- a/sdk/js/generated/api.json +++ b/sdk/js/generated/api.json @@ -28247,7 +28247,7 @@ }, { "name": "operating_channel_width", - "description": "Operating Channel Width (kHz).", + "description": "Operating Channel Width (Hz).", "label": "", "type": "uint32", "longType": "uint32",