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",