diff --git a/CHANGELOG.md b/CHANGELOG.md index 90cbdd8c83..8487c72bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ For details about compatibility between different releases, see the **Commitment ### Added - `gs_gateways_disconnected_total` metric, counting gateway disconnections by protocol and by the error the connection was disconnected with. This makes disconnection reasons (such as gateways disappearing without a close handshake, or missing too many pongs) observable as a rate, instead of only through logs. +- Downlink scheduling on all antennas of a gateway. Previously, only the first antenna could be used as a downlink path, and uplinks received on any other antenna had their downlink path disabled. See [issue #48](https://github.com/TheThingsNetwork/lorawan-stack/issues/48) for more context. ### Changed @@ -26,6 +27,7 @@ For details about compatibility between different releases, see the **Commitment ### Fixed - Parsing of the `rctx` field in the `upinfo` object of upstream messages received via the LoRa Basics Station LNS protocol. The field name was misspelled as `rtcx`, so the radio context reported by gateways was ignored. The antenna index in the uplink metadata now reflects the reported radio context and is echoed back in class A downlinks, instead of always being 0. +- The antenna gain sent to LoRa Basics Station gateways in the router configuration. Each board's radio configuration now uses the gain of its own antenna instead of applying the first antenna's gain to all boards, and fractional gains are no longer truncated. ### Security diff --git a/pkg/gatewayserver/io/io.go b/pkg/gatewayserver/io/io.go index 255577ba69..b150757d8a 100644 --- a/pkg/gatewayserver/io/io.go +++ b/pkg/gatewayserver/io/io.go @@ -116,6 +116,7 @@ type Connection struct { gateway *ttnpb.Gateway gatewayPrimaryFP *frequencyplans.FrequencyPlan gatewayFPs []*frequencyplans.FrequencyPlan + antennaGains []float32 band *band.Band fps *frequencyplans.Store scheduler *scheduling.Scheduler @@ -216,6 +217,25 @@ func NewConnection( } } + var fallbackAntennaGain float32 + if len(gateway.Antennas) > 0 && gateway.Antennas[0] != nil { + fallbackAntennaGain = gateway.Antennas[0].Gain + } + + // antennaGains is aligned index-for-index with gatewayFPs: antennaGains[i] is the gain for the + // board configured by gatewayFPs[i]. Physically each board carries exactly one antenna, but the + // registry does not enforce that the antennas and frequency plans of a gateway are registered + // consistently, so a gateway may have fewer antennas registered than frequency plans. In that + // case the gain of the first antenna is used as a fallback. + antennaGains := make([]float32, len(gatewayFPs)) + for i := range gatewayFPs { + if i < len(gateway.Antennas) && gateway.Antennas[i] != nil { + antennaGains[i] = gateway.Antennas[i].Gain + continue + } + antennaGains[i] = fallbackAntennaGain + } + ctx, cancelCtx := errorcontext.New(ctx) scheduler, err := scheduling.NewScheduler( ctx, gatewayFPs, enforceDutyCycle, frontend.DutyCycleStyle(), scheduleAnytimeDelay, nil, @@ -232,6 +252,7 @@ func NewConnection( gateway: gateway, gatewayPrimaryFP: fp0, gatewayFPs: gatewayFPs, + antennaGains: antennaGains, band: &phy, fps: fps, scheduler: scheduler, @@ -342,11 +363,6 @@ func (c *Connection) HandleUp(up *ttnpb.UplinkMessage, frontendSync *FrontendClo for _, md := range up.RxMetadata { md.ReceivedAt = timestamppb.New(receivedAtGateway) - if md.AntennaIndex != 0 { - // TODO: Support downlink path to multiple antennas (https://github.com/TheThingsNetwork/lorawan-stack/issues/48) - md.DownlinkPathConstraint = ttnpb.DownlinkPathConstraint_DOWNLINK_PATH_CONSTRAINT_NEVER - continue - } buf, err := UplinkToken( &ttnpb.GatewayAntennaIdentifiers{ GatewayIds: c.gateway.GetIds(), @@ -877,6 +893,12 @@ func (c *Connection) PrimaryFrequencyPlan() *frequencyplans.FrequencyPlan { retu // TODO: Handle mixed bands (https://github.com/TheThingsNetwork/lorawan-stack/issues/1394) func (c *Connection) BandID() string { return c.band.ID } +// AntennaGains returns the antenna gains of the gateway, aligned index-for-index with the +// frequency plans returned by FrequencyPlans(). When the gateway has fewer registered antennas +// than frequency plans, the first antenna's gain (or 0 when there are no antennas) is used for +// the remaining entries. +func (c *Connection) AntennaGains() []float32 { return c.antennaGains } + // SyncWithGatewayConcentrator synchronizes the clock with the given concentrator timestamp, the server time and the // relative gateway time that corresponds to the given timestamp. func (c *Connection) SyncWithGatewayConcentrator(timestamp uint32, server time.Time, gateway *time.Time, concentrator scheduling.ConcentratorTime) scheduling.ConcentratorTime { diff --git a/pkg/gatewayserver/io/io_test.go b/pkg/gatewayserver/io/io_test.go index 9e1ab2b691..165b483b22 100644 --- a/pkg/gatewayserver/io/io_test.go +++ b/pkg/gatewayserver/io/io_test.go @@ -547,6 +547,62 @@ func TestFlow(t *testing.T) { } } +func TestNewConnectionAntennaGains(t *testing.T) { + ctx := test.Context() + frontend := &mock.Frontend{} + addr := &ttnpb.GatewayRemoteAddress{Ip: "127.0.0.1"} + + for _, tc := range []struct { + Name string + Antennas []*ttnpb.GatewayAntenna + ExpectedGains []float32 + }{ + { + Name: "MatchesFrequencyPlans", + Antennas: []*ttnpb.GatewayAntenna{ + {Gain: 3}, + {Gain: 5}, + }, + ExpectedGains: []float32{3, 5}, + }, + { + Name: "FewerAntennasThanFrequencyPlans", + Antennas: []*ttnpb.GatewayAntenna{ + {Gain: 3}, + }, + ExpectedGains: []float32{3, 3}, + }, + { + Name: "NilAntennaFallsBackToFirst", + Antennas: []*ttnpb.GatewayAntenna{ + {Gain: 3}, + nil, + }, + ExpectedGains: []float32{3, 3}, + }, + { + Name: "NoAntennas", + Antennas: nil, + ExpectedGains: []float32{0, 0}, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + a := assertions.New(t) + gtw := &ttnpb.Gateway{ + Ids: &ttnpb.GatewayIdentifiers{GatewayId: "antenna-gain-test"}, + FrequencyPlanId: "EU_863_870", + FrequencyPlanIds: []string{"EU_863_870", "EU_863_870"}, + Antennas: tc.Antennas, + } + conn, err := io.NewConnection(ctx, frontend, gtw, test.FrequencyPlanStore, true, nil, addr) + if !a.So(err, should.BeNil) { + t.FailNow() + } + a.So(conn.AntennaGains(), should.Resemble, tc.ExpectedGains) + }) + } +} + func TestSubBandEIRPOverride(t *testing.T) { a := assertions.New(t) ctx := log.NewContext(test.Context(), test.GetLogger(t)) diff --git a/pkg/gatewayserver/io/semtechws/lbslns/upstream.go b/pkg/gatewayserver/io/semtechws/lbslns/upstream.go index f682c92c34..a3eb94d885 100644 --- a/pkg/gatewayserver/io/semtechws/lbslns/upstream.go +++ b/pkg/gatewayserver/io/semtechws/lbslns/upstream.go @@ -560,16 +560,9 @@ func (f *lbsLNS) HandleUp( // nolint:gocyclo switch typ { case TypeUpstreamVersion: - var antennaGain int - antennas := conn.Gateway().Antennas - if len(antennas) != 0 && antennas[0] != nil { - // TODO: Support downlink path to multiple antennas (https://github.com/TheThingsNetwork/lorawan-stack/issues/48). - // Need to set different gain value to each `SX1301_conf` object as per https://doc.sm.tc/station/gw_v1.5.html#multi-board-sample-configuration. - // Currently we support downlink to only one antenna so the gain of the first antenna is applied to all (though the latter ones are not used). - // FPs and Antennas need to be synchronized. See https://github.com/TheThingsNetwork/lorawan-stack/issues/48#issuecomment-983412639. - antennaGain = int(antennas[0].Gain) - } - ctx, msg, stat, err := f.GetRouterConfig(ctx, raw, conn.BandID(), conn.FrequencyPlans(), antennaGain, receivedAt) + ctx, msg, stat, err := f.GetRouterConfig( + ctx, raw, conn.BandID(), conn.FrequencyPlans(), conn.AntennaGains(), receivedAt, + ) if err != nil { logger.WithError(err).Warn("Failed to generate router configuration") return nil, err diff --git a/pkg/gatewayserver/io/semtechws/lbslns/version.go b/pkg/gatewayserver/io/semtechws/lbslns/version.go index b87d069969..e8aee0ebba 100644 --- a/pkg/gatewayserver/io/semtechws/lbslns/version.go +++ b/pkg/gatewayserver/io/semtechws/lbslns/version.go @@ -64,7 +64,7 @@ func (*lbsLNS) GetRouterConfig( msg []byte, bandID string, fps []*frequencyplans.FrequencyPlan, - antennaGain int, + antennaGains []float32, receivedAt time.Time, ) (context.Context, []byte, *ttnpb.GatewayStatus, error) { var version Version @@ -76,7 +76,7 @@ func (*lbsLNS) GetRouterConfig( // to gateways that signal the presence of a PPS. // References https://github.com/lorabasics/basicstation/issues/135. semtechws.UpdateSessionTimeSync(ctx, true) - cfg, err := pfconfig.GetRouterConfig(ctx, bandID, fps, version, time.Now(), antennaGain) + cfg, err := pfconfig.GetRouterConfig(ctx, bandID, fps, version, time.Now(), antennaGains) if err != nil { return ctx, nil, nil, err } diff --git a/pkg/pfconfig/lbslns/lbslbs_test.go b/pkg/pfconfig/lbslns/lbslbs_test.go index e08d9b2377..8279e9c888 100644 --- a/pkg/pfconfig/lbslns/lbslbs_test.go +++ b/pkg/pfconfig/lbslns/lbslbs_test.go @@ -271,7 +271,7 @@ func TestGetRouterConfig(t *testing.T) { a, ctx := test.New(t) fps := []*frequencyplans.FrequencyPlan{&tc.FrequencyPlan} - cfg, err := GetRouterConfig(ctx, tc.FrequencyPlan.BandID, fps, tc.Features, time.Now(), 0) + cfg, err := GetRouterConfig(ctx, tc.FrequencyPlan.BandID, fps, tc.Features, time.Now(), nil) if err != nil { if tc.ErrorAssertion == nil || !a.So(tc.ErrorAssertion(err), should.BeTrue) { t.Fatalf("Unexpected error: %v", err) @@ -295,13 +295,15 @@ func TestGetRouterConfigWithMultipleFP(t *testing.T) { Name string BandID string FrequencyPlans []*frequencyplans.FrequencyPlan + AntennaGains []float32 Cfg RouterConfig Features TestFeatures ErrorAssertion func(err error) bool }{ { - Name: "ValidFrequencyPlan", - BandID: "US_902_928", + Name: "ValidFrequencyPlan", + BandID: "US_902_928", + AntennaGains: []float32{3, 3}, FrequencyPlans: []*frequencyplans.FrequencyPlan{ { BandID: "US_902_928", @@ -463,12 +465,177 @@ func TestGetRouterConfigWithMultipleFP(t *testing.T) { }, }, }, + { + Name: "DistinctAntennaGains", + BandID: "US_902_928", + AntennaGains: []float32{3, 6}, + FrequencyPlans: []*frequencyplans.FrequencyPlan{ + { + BandID: "US_902_928", + Radios: []frequencyplans.Radio{ + { + Enable: true, + ChipType: "SX1257", + Frequency: 924300000, + TxConfiguration: &frequencyplans.RadioTxConfiguration{ + MinFrequency: 909000000, + MaxFrequency: 927000000, + }, + }, + { + Enable: false, + ChipType: "SX1257", + Frequency: 925000000, + }, + }, + }, + { + BandID: "US_902_928", + Radios: []frequencyplans.Radio{ + { + Enable: true, + ChipType: "SX1257", + Frequency: 924300000, + TxConfiguration: &frequencyplans.RadioTxConfiguration{ + MinFrequency: 900000000, + MaxFrequency: 925000000, + }, + }, + { + Enable: false, + ChipType: "SX1257", + Frequency: 925000000, + }, + }, + }, + }, + Cfg: RouterConfig{ + Region: "US902", + HardwareSpec: "sx1301/2", + FrequencyRange: []int{900000000, 927000000}, + DataRates: DataRates{ + [3]int{10, 125, 0}, + [3]int{9, 125, 0}, + [3]int{8, 125, 0}, + [3]int{7, 125, 0}, + [3]int{8, 500, 0}, + [3]int{0, 0, 0}, + [3]int{0, 0, 0}, + [3]int{0, 0, 0}, + [3]int{12, 500, 0}, + [3]int{11, 500, 0}, + [3]int{10, 500, 0}, + [3]int{9, 500, 0}, + [3]int{8, 500, 0}, + [3]int{7, 500, 0}, + }, + NoCCA: true, + NoDutyCycle: true, + NoDwellTime: true, + SX1301Config: []LBSSX1301Config{ + { + Radios: []LBSRFConfig{ + { + Enable: true, + Frequency: 924300000, + AntennaGain: 3, + }, + { + Enable: false, + Frequency: 925000000, + AntennaGain: 3, + }, + }, + Channels: []shared.IFConfig{ + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + }, + LoRaStandardChannel: &shared.IFConfig{ + Enable: false, + Radio: 0, + IFValue: 0, + Bandwidth: 0, + SpreadFactor: 0, + Datarate: 0, + }, + FSKChannel: &shared.IFConfig{ + Enable: false, + Radio: 0, + IFValue: 0, + Bandwidth: 0, + SpreadFactor: 0, + Datarate: 0, + }, + }, + { + Radios: []LBSRFConfig{ + { + Enable: true, + Frequency: 924300000, + AntennaGain: 6, + }, + { + Enable: false, + Frequency: 925000000, + AntennaGain: 6, + }, + }, + Channels: []shared.IFConfig{ + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + {Enable: false, Radio: 0, IFValue: 0, Bandwidth: 0, SpreadFactor: 0, Datarate: 0}, + }, + LoRaStandardChannel: &shared.IFConfig{ + Enable: false, + Radio: 0, + IFValue: 0, + Bandwidth: 0, + SpreadFactor: 0, + Datarate: 0, + }, + FSKChannel: &shared.IFConfig{ + Enable: false, + Radio: 0, + IFValue: 0, + Bandwidth: 0, + SpreadFactor: 0, + Datarate: 0, + }, + }, + }, + Beacon: &BeaconingConfig{ + DR: ttnpb.DataRateIndex_DATA_RATE_8, + Layout: [3]int{5, 11, 23}, + Freqs: []uint64{ + 923300000, + 923900000, + 924500000, + 925100000, + 925700000, + 926300000, + 926900000, + 927500000, + }, + }, + }, + }, } { t.Run(tc.Name, func(t *testing.T) { t.Parallel() a, ctx := test.New(t) - cfg, err := GetRouterConfig(ctx, tc.BandID, tc.FrequencyPlans, tc.Features, time.Now(), 3) + cfg, err := GetRouterConfig(ctx, tc.BandID, tc.FrequencyPlans, tc.Features, time.Now(), tc.AntennaGains) if err != nil { if tc.ErrorAssertion == nil || !a.So(tc.ErrorAssertion(err), should.BeTrue) { t.Fatalf("Unexpected error: %v", err) diff --git a/pkg/pfconfig/lbslns/lbslns.go b/pkg/pfconfig/lbslns/lbslns.go index 3ba01ace03..3a7e2eb099 100644 --- a/pkg/pfconfig/lbslns/lbslns.go +++ b/pkg/pfconfig/lbslns/lbslns.go @@ -100,9 +100,9 @@ type DataRates [16][3]int // - `tx_freq_min` and `tx_freq_max` are defined in the `freq_range` parameter of `router_config`. // - `antenna_gain` is defined by the user for each antenna. type LBSRFConfig struct { - Enable bool `json:"enable"` - Frequency uint64 `json:"freq"` - AntennaGain int `json:"antenna_gain,omitempty"` + Enable bool `json:"enable"` + Frequency uint64 `json:"freq"` + AntennaGain float32 `json:"antenna_gain,omitempty"` } // LBSSX1301Config contains the configuration for the SX1301 concentrator for @@ -141,7 +141,7 @@ func (c LBSSX1301Config) MarshalJSON() ([]byte, error) { } // fromSX1301Conf updates fields from shared.SX1301Config. -func (c *LBSSX1301Config) fromSX1301Conf(sx1301Conf shared.SX1301Config, antennaGain int) { +func (c *LBSSX1301Config) fromSX1301Conf(sx1301Conf shared.SX1301Config, antennaGain float32) { c.LoRaStandardChannel = sx1301Conf.LoRaStandardChannel c.FSKChannel = sx1301Conf.FSKChannel c.LBTConfig = sx1301Conf.LBTConfig @@ -271,13 +271,14 @@ type RouterFeatures interface { // GetRouterConfig returns the routerconfig message to be sent to the gateway. // Currently as per the LBS docs, all frequency plans have to be from the same region (band). // https://doc.sm.tc/station/tcproto.html#router-config-message. +// antennaGains is aligned index-for-index with fps, and missing entries default to 0. func GetRouterConfig( ctx context.Context, bandID string, fps []*frequencyplans.FrequencyPlan, features RouterFeatures, dlTime time.Time, - antennaGain int, + antennaGains []float32, ) (RouterConfig, error) { for _, fp := range fps { if err := fp.Validate(); err != nil { @@ -324,7 +325,7 @@ func GetRouterConfig( conf.NoDutyCycle = !production conf.NoDwellTime = !production - for _, fp := range fps { + for i, fp := range fps { if len(fp.Radios) == 0 { continue } @@ -332,6 +333,10 @@ func GetRouterConfig( if err != nil { return RouterConfig{}, err } + var antennaGain float32 + if i < len(antennaGains) { + antennaGain = antennaGains[i] + } var lbsSX1301Config LBSSX1301Config lbsSX1301Config.fromSX1301Conf(*sx1301Conf, antennaGain) conf.SX1301Config = append(conf.SX1301Config, lbsSX1301Config)