Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
32 changes: 27 additions & 5 deletions pkg/gatewayserver/io/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -232,6 +252,7 @@ func NewConnection(
gateway: gateway,
gatewayPrimaryFP: fp0,
gatewayFPs: gatewayFPs,
antennaGains: antennaGains,
band: &phy,
fps: fps,
scheduler: scheduler,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions pkg/gatewayserver/io/io_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,62 @@
}
}

func TestNewConnectionAntennaGains(t *testing.T) {

Check failure on line 550 in pkg/gatewayserver/io/io_test.go

View workflow job for this annotation

GitHub Actions / Code Quality

Function TestNewConnectionAntennaGains missing the call to method parallel (paralleltest)

Check failure on line 550 in pkg/gatewayserver/io/io_test.go

View workflow job for this annotation

GitHub Actions / Code Quality

Function TestNewConnectionAntennaGains missing the call to method parallel (paralleltest)
ctx := test.Context()
frontend := &mock.Frontend{}
addr := &ttnpb.GatewayRemoteAddress{Ip: "127.0.0.1"}

for _, tc := range []struct {

Check failure on line 555 in pkg/gatewayserver/io/io_test.go

View workflow job for this annotation

GitHub Actions / Code Quality

Range statement for test TestNewConnectionAntennaGains missing the call to method parallel in test Run (paralleltest)

Check failure on line 555 in pkg/gatewayserver/io/io_test.go

View workflow job for this annotation

GitHub Actions / Code Quality

Range statement for test TestNewConnectionAntennaGains missing the call to method parallel in test Run (paralleltest)
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))
Expand Down
13 changes: 3 additions & 10 deletions pkg/gatewayserver/io/semtechws/lbslns/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/gatewayserver/io/semtechws/lbslns/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
175 changes: 171 additions & 4 deletions pkg/pfconfig/lbslns/lbslbs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading