Skip to content

Commit

Permalink
Implement locking of gateway for Class-C downlink.
Browse files Browse the repository at this point in the history
In case the device is covered by a single half-duplex gateway, this
setting makes it possible to lock the gateway for a certain time after
each Class-C downlink scheduling to avoid continuously sending Class-C
and therefore not being able to receive any uplink (responses).
  • Loading branch information
brocaar committed May 5, 2021
1 parent ad35971 commit 3f40562
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 15 deletions.
14 changes: 12 additions & 2 deletions cmd/chirpstack-network-server/cmd/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,21 @@ get_downlink_data_delay="{{ .NetworkServer.GetDownlinkDataDelay }}"
# Class-C settings.
[network_server.scheduler.class_c]
# Downlink lock duration
# Device downlink lock duration
#
# Contains the duration to lock the downlink Class-C transmissions
# after a preceeding downlink tx (per device).
downlink_lock_duration="{{ .NetworkServer.Scheduler.ClassC.DownlinkLockDuration }}"
device_downlink_lock_duration="{{ .NetworkServer.Scheduler.ClassC.DeviceDownlinkLockDuration }}"
# Gateway downlink lock duration.
#
# Contains the duration to lock the downlink Class-C transmissions
# after a preceeding downlink tx (per gateway). As half-duplex gateways
# can't receive when transmitting, this value can be used to avoid that
# a single gateway will transmit multiple frames directly after each other
# and because of that, unable to receive any uplinks.
gateway_downlink_lock_duration="{{ .NetworkServer.Scheduler.ClassC.GatewayDownlinkLockDuration }}"
# Multicast gateway delay.
#
Expand Down
2 changes: 1 addition & 1 deletion cmd/chirpstack-network-server/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func init() {
viper.SetDefault("network_server.gateway.backend.type", "mqtt")

viper.SetDefault("network_server.scheduler.scheduler_interval", 1*time.Second)
viper.SetDefault("network_server.scheduler.class_c.downlink_lock_duration", 2*time.Second)
viper.SetDefault("network_server.scheduler.class_c.device_downlink_lock_duration", 2*time.Second)
viper.SetDefault("network_server.scheduler.class_c.multicast_gateway_delay", 2*time.Second)

viper.SetDefault("network_server.gateway.client_cert_lifetime", time.Hour*24*365)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/ns/network_server_new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func (ts *NetworkServerAPITestSuite) TestMulticastQueue() {
if i == 0 {
continue
}
lockDuration := config.C.NetworkServer.Scheduler.ClassC.DownlinkLockDuration
lockDuration := config.C.NetworkServer.Scheduler.ClassC.DeviceDownlinkLockDuration
assert.Equal(scheduleAt, items[i].ScheduleAt.Add(-lockDuration))
scheduleAt = items[i].ScheduleAt
}
Expand Down
5 changes: 3 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ type Config struct {
SchedulerInterval time.Duration `mapstructure:"scheduler_interval"`

ClassC struct {
DownlinkLockDuration time.Duration `mapstructure:"downlink_lock_duration"`
MulticastGatewayDelay time.Duration `mapstructure:"multicast_gateway_delay"`
GatewayDownlinkLockDuration time.Duration `mapstructure:"gateway_downlink_lock_duration"`
DeviceDownlinkLockDuration time.Duration `mapstructure:"device_downlink_lock_duration"`
MulticastGatewayDelay time.Duration `mapstructure:"multicast_gateway_delay"`
} `mapstructure:"class_c"`
} `mapstructure:"scheduler"`

Expand Down
42 changes: 36 additions & 6 deletions internal/downlink/data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ import (
)

const (
defaultCodeRate = "4/5"
downlinkLockKey = "lora:ns:device:%s:down:lock"
defaultCodeRate = "4/5"
deviceDownlinkLockKey = "lora:ns:device:%s:down:lock"
gatewayDownlinkLockKey = "lora:ns:gw:%s:down:lock"
)

type incompatibleCIDMapping struct {
Expand Down Expand Up @@ -84,7 +85,8 @@ var (
disableADR bool

// ClassC
classCDownlinkLockDuration time.Duration
classCDeviceDownlinkLockDuration time.Duration
classCGatewayDownlinkLockDuration time.Duration

// Dwell time.
uplinkDwellTime400ms bool
Expand Down Expand Up @@ -140,6 +142,7 @@ var scheduleNextQueueItemTasks = []func(*dataContext) error{
setDeviceGatewayRXInfo,
selectDownlinkGateway,
forClass(storage.DeviceModeC,
getDownlinkGatewayLock,
setImmediately,
setTXInfoForRX2,
),
Expand Down Expand Up @@ -184,7 +187,8 @@ func Setup(conf config.Config) error {
disableMACCommands = nsConf.DisableMACCommands
disableADR = nsConf.DisableADR

classCDownlinkLockDuration = conf.NetworkServer.Scheduler.ClassC.DownlinkLockDuration
classCDeviceDownlinkLockDuration = conf.NetworkServer.Scheduler.ClassC.DeviceDownlinkLockDuration
classCGatewayDownlinkLockDuration = conf.NetworkServer.Scheduler.ClassC.GatewayDownlinkLockDuration

uplinkDwellTime400ms = conf.NetworkServer.Band.UplinkDwellTime400ms
downlinkDwellTime400ms = conf.NetworkServer.Band.DownlinkDwellTime400ms
Expand Down Expand Up @@ -1573,13 +1577,39 @@ func setDeviceGatewayRXInfo(ctx *dataContext) error {
// scheduling where the queue items are sent immediately / as soon as possible.
// This avoids race-conditions when running multiple NS instances.
func getDownlinkDeviceLock(ctx *dataContext) error {
key := storage.GetRedisKey(downlinkLockKey, ctx.DeviceSession.DevEUI)
set, err := storage.RedisClient().SetNX(key, "lock", classCDownlinkLockDuration).Result()
key := storage.GetRedisKey(deviceDownlinkLockKey, ctx.DeviceSession.DevEUI)
set, err := storage.RedisClient().SetNX(key, "lock", classCDeviceDownlinkLockDuration).Result()
if err != nil {
return errors.Wrap(err, "acquire downlink device lock error")
}

if !set {
// the device is already locked
return ErrAbort
}

return nil
}

// getDownlinkGatewayLock acquires a downlink gateway lock. This can be useful
// to make sure that half-duplex gateways don't end up sending downlinks continuously
// and missing uplink responses to transmitted Class-C downlinks.
func getDownlinkGatewayLock(ctx *dataContext) error {
// nothing to do when it is not configured
if classCGatewayDownlinkLockDuration == 0 {
return nil
}

var id lorawan.EUI64
copy(id[:], ctx.DownlinkFrame.GatewayId)
key := storage.GetRedisKey(gatewayDownlinkLockKey, id)
set, err := storage.RedisClient().SetNX(key, "lock", classCGatewayDownlinkLockDuration).Result()
if err != nil {
return errors.Wrap(err, "acquire downlink gateway lock error")
}

if !set {
// the gateway is already locked
return ErrAbort
}

Expand Down
2 changes: 1 addition & 1 deletion internal/downlink/multicast/enqueue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (ts *EnqueueQueueItemTestCase) TestClassC() {
assert.Nil(items[0].EmitAtTimeSinceGPSEpoch)
assert.Nil(items[1].EmitAtTimeSinceGPSEpoch)

lockDuration := config.C.NetworkServer.Scheduler.ClassC.DownlinkLockDuration
lockDuration := config.C.NetworkServer.Scheduler.ClassC.DeviceDownlinkLockDuration
assert.EqualValues(math.Abs(float64(items[0].ScheduleAt.Sub(items[1].ScheduleAt))), lockDuration)

mg, err := storage.GetMulticastGroup(context.Background(), ts.tx, ts.MulticastGroup.ID, false)
Expand Down
3 changes: 2 additions & 1 deletion internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ func GetConfig() config.Config {
c.NetworkServer.NetworkSettings.MaxMACCommandErrorCount = 3

c.NetworkServer.Scheduler.SchedulerInterval = time.Second
c.NetworkServer.Scheduler.ClassC.DownlinkLockDuration = time.Second * 3
c.NetworkServer.Scheduler.ClassC.DeviceDownlinkLockDuration = time.Second * 3
c.NetworkServer.Scheduler.ClassC.GatewayDownlinkLockDuration = time.Second * 3

c.NetworkServer.Gateway.Backend.MultiDownlinkFeature = "multi_only"
c.NetworkServer.Gateway.Backend.MQTT.Server = "tcp://127.0.0.1:1883"
Expand Down
9 changes: 9 additions & 0 deletions internal/testsuite/assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@ func AssertDownlinkDeviceLock(devEUI lorawan.EUI64) Assertion {
}
}

// AssertDownlinkGatewayLock asserts that a downlink lock for the given gateway exists.
func AssertDownlinkGatewayLock(gatewayID lorawan.EUI64) Assertion {
return func(assert *require.Assertions, ts *IntegrationTestSuite) {
key := storage.GetRedisKey("lora:ns:gw:%s:down:lock", gatewayID)
err := storage.RedisClient().Get(key).Err()
assert.NoError(err)
}
}

// AssertJSJoinReq asserts the given join-server JoinReq.
func AssertJSJoinReqPayload(pl backend.JoinReqPayload) Assertion {
return func(assert *require.Assertions, ts *IntegrationTestSuite) {
Expand Down
1 change: 1 addition & 0 deletions internal/testsuite/class_c_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (ts *ClassCTestSuite) TestClassC() {
},
},
}),
AssertDownlinkGatewayLock(ts.Gateway.GatewayID),
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion internal/uplink/data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ var (
func Setup(conf config.Config) error {
getDownlinkDataDelay = conf.NetworkServer.GetDownlinkDataDelay
disableMACCommands = conf.NetworkServer.NetworkSettings.DisableMACCommands
classCDownlinkLockDuration = conf.NetworkServer.Scheduler.ClassC.DownlinkLockDuration
classCDownlinkLockDuration = conf.NetworkServer.Scheduler.ClassC.DeviceDownlinkLockDuration

return nil
}
Expand Down

0 comments on commit 3f40562

Please sign in to comment.