Commit 2aea7096 authored by Orne Brocaar's avatar Orne Brocaar
Browse files

Add LR-FHSS only and LoRa & LR-FHSS ADR algorithms.

parent 69ba0766
......@@ -83,6 +83,7 @@ type HandleResponse struct {
type UplinkMetaData struct {
FCnt uint32
MaxSNR float32
MaxRSSI int32
TXPowerIndex int
GatewayCount int
}
......
......@@ -8,7 +8,7 @@ require (
github.com/Azure/azure-service-bus-go v0.9.1
github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5
github.com/brocaar/chirpstack-api/go/v3 v3.12.5
github.com/brocaar/lorawan v0.0.0-20211213100234-63df6954a2f3
github.com/brocaar/lorawan v0.0.0-20220207095711-d675789e16ab
github.com/eclipse/paho.mqtt.golang v1.2.0
github.com/go-redis/redis/v8 v8.8.3
github.com/gofrs/uuid v3.2.0+incompatible
......
......@@ -17,16 +17,28 @@ var (
)
func init() {
def := &DefaultHandler{}
id, _ := def.ID()
name, _ := def.Name()
defH := &DefaultHandler{}
defID, _ := defH.ID()
defName, _ := defH.Name()
lrFHSSH := &LRFHSSHandler{}
lrFHSSID, _ := lrFHSSH.ID()
lrFHSSName, _ := lrFHSSH.Name()
loRaLRFHSSH := &LoRaLRFHSSHandler{}
loraLRFHSSID, _ := loRaLRFHSSH.ID()
loraLRFHSSName, _ := loRaLRFHSSH.Name()
handlers = map[string]adr.Handler{
id: def,
defID: defH,
loraLRFHSSID: loRaLRFHSSH,
lrFHSSID: lrFHSSH,
}
handlerNames = map[string]string{
id: name,
defID: defName,
loraLRFHSSID: loraLRFHSSName,
lrFHSSID: lrFHSSName,
}
}
......
package adr
import "github.com/brocaar/chirpstack-network-server/v3/adr"
import (
"github.com/brocaar/chirpstack-network-server/v3/adr"
"github.com/brocaar/chirpstack-network-server/v3/internal/band"
loraband "github.com/brocaar/lorawan/band"
)
// DefaultHandler implements the default ADR handler.
type DefaultHandler struct{}
......@@ -12,7 +16,7 @@ func (h *DefaultHandler) ID() (string, error) {
// Name returns the default name.
func (h *DefaultHandler) Name() (string, error) {
return "Default ADR algorithm", nil
return "Default ADR algorithm (LoRa only)", nil
}
// Handle handles the ADR request.
......@@ -30,9 +34,31 @@ func (h *DefaultHandler) Handle(req adr.HandleRequest) (adr.HandleResponse, erro
return resp, nil
}
// The max DR might be configured to a non LoRa (125kHz) data-rate.
// As this algorithm works on LoRa (125kHz) data-rates only, we need to
// find the max LoRa (125 kHz) data-rate.
maxDR := req.MaxDR
maxLoRaDR := 0
enabledDRs := band.Band().GetEnabledUplinkDataRates()
for _, i := range enabledDRs {
dr, err := band.Band().GetDataRate(i)
if err != nil {
return resp, err
}
if dr.Modulation == loraband.LoRaModulation && dr.Bandwidth == 125 {
maxLoRaDR = i
}
}
// Reduce to max LoRa DR.
if maxDR > maxLoRaDR {
maxDR = maxLoRaDR
}
// Lower the DR only if it exceeds the max. allowed DR.
if req.DR > req.MaxDR {
resp.DR = req.MaxDR
if req.DR > maxDR {
resp.DR = maxDR
}
// Set the new NbTrans.
......@@ -50,7 +76,7 @@ func (h *DefaultHandler) Handle(req adr.HandleRequest) (adr.HandleResponse, erro
return resp, nil
}
resp.TxPowerIndex, resp.DR = h.getIdealTxPowerIndexAndDR(nStep, resp.TxPowerIndex, resp.DR, req.MaxTxPowerIndex, req.MaxDR)
resp.TxPowerIndex, resp.DR = h.getIdealTxPowerIndexAndDR(nStep, resp.TxPowerIndex, resp.DR, req.MaxTxPowerIndex, maxDR)
return resp, nil
}
......
......@@ -5,6 +5,8 @@ import (
"testing"
"github.com/brocaar/chirpstack-network-server/v3/adr"
"github.com/brocaar/chirpstack-network-server/v3/internal/band"
"github.com/brocaar/chirpstack-network-server/v3/internal/test"
"github.com/stretchr/testify/require"
)
......@@ -275,6 +277,9 @@ func TestDefaultHandler(t *testing.T) {
}
for _, tst := range tests {
conf := test.GetConfig()
band.Setup(conf)
t.Run(tst.name, func(t *testing.T) {
assert := require.New(t)
......
package adr
import (
"github.com/brocaar/chirpstack-network-server/v3/adr"
"github.com/brocaar/chirpstack-network-server/v3/internal/band"
)
// LoRaLRFHSSHandler implements a LoRa / LR-FHSS ADR handler.
type LoRaLRFHSSHandler struct{}
// ID returns the ID.
func (h *LoRaLRFHSSHandler) ID() (string, error) {
return "lora_lr_fhss", nil
}
// Name returns the name.
func (h *LoRaLRFHSSHandler) Name() (string, error) {
return "LoRa & LR-FHSS ADR algorithm", nil
}
// Handle handles the ADR request.
func (h *LoRaLRFHSSHandler) Handle(req adr.HandleRequest) (adr.HandleResponse, error) {
resp := adr.HandleResponse{
DR: req.DR,
TxPowerIndex: req.TxPowerIndex,
NbTrans: req.NbTrans,
}
band := band.Band()
loRaHandler := DefaultHandler{}
lrFHSSHandler := LRFHSSHandler{}
loRaResp, err := loRaHandler.Handle(req)
if err != nil {
return resp, err
}
loRaDR, err := band.GetDataRate(loRaResp.DR)
if err != nil {
return resp, err
}
lrFHSSResp, err := lrFHSSHandler.Handle(req)
if err != nil {
return resp, err
}
// For SF < 10, LoRa is a better option, for SF >= 10 use LR-FHSS.
if loRaDR.SpreadFactor < 10 {
return loRaResp, nil
}
return lrFHSSResp, nil
}
package adr
import (
"testing"
"github.com/brocaar/chirpstack-network-server/v3/adr"
"github.com/brocaar/chirpstack-network-server/v3/internal/band"
"github.com/brocaar/chirpstack-network-server/v3/internal/test"
"github.com/stretchr/testify/require"
)
func TestLoRaLRFHSSHandler(t *testing.T) {
h := &LoRaLRFHSSHandler{}
t.Run("ID", func(t *testing.T) {
assert := require.New(t)
id, err := h.ID()
assert.NoError(err)
assert.Equal("lora_lr_fhss", id)
})
t.Run("Handle", func(t *testing.T) {
conf := test.GetConfig()
// Add channel with LR-FHSS data-rate enabled.
conf.NetworkServer.NetworkSettings.ExtraChannels = append(conf.NetworkServer.NetworkSettings.ExtraChannels, struct {
Frequency uint32 `mapstructure:"frequency"`
MinDR int `mapstructure:"min_dr"`
MaxDR int `mapstructure:"max_dr"`
}{
Frequency: 867300000,
MinDR: 10,
MaxDR: 11,
})
band.Setup(conf)
tests := []struct {
name string
request adr.HandleRequest
response adr.HandleResponse
}{
{
name: "switch to DR 3 (LoRa)",
request: adr.HandleRequest{
ADR: true,
DR: 0,
NbTrans: 1,
MaxDR: 11,
RequiredSNRForDR: -20,
UplinkHistory: []adr.UplinkMetaData{
{
MaxSNR: -10,
},
},
},
response: adr.HandleResponse{
DR: 3,
NbTrans: 1,
},
},
{
name: "switch to DR 3 (LoRa)",
request: adr.HandleRequest{
ADR: true,
DR: 0,
NbTrans: 3,
MaxDR: 11,
RequiredSNRForDR: -20,
UplinkHistory: []adr.UplinkMetaData{
{
MaxSNR: -12,
},
},
},
response: adr.HandleResponse{
DR: 10,
NbTrans: 1,
},
},
}
for _, tst := range tests {
t.Run(tst.name, func(t *testing.T) {
assert := require.New(t)
resp, err := h.Handle(tst.request)
assert.NoError(err)
assert.Equal(tst.response, resp)
})
}
})
}
package adr
import (
"math/rand"
"sort"
"time"
"github.com/brocaar/chirpstack-network-server/v3/adr"
"github.com/brocaar/chirpstack-network-server/v3/internal/band"
loraband "github.com/brocaar/lorawan/band"
)
// LRFHSSHandler implements a LR-FHSS only ADR handler.
type LRFHSSHandler struct{}
// ID returns the ID.
func (h *LRFHSSHandler) ID() (string, error) {
return "lr_fhss", nil
}
// Name returns the name.
func (h *LRFHSSHandler) Name() (string, error) {
return "LR-FHSS only ADR algorithm", nil
}
// Handle handles the ADR request.
func (h *LRFHSSHandler) Handle(req adr.HandleRequest) (adr.HandleResponse, error) {
resp := adr.HandleResponse{
DR: req.DR,
TxPowerIndex: req.TxPowerIndex,
NbTrans: req.NbTrans,
}
if !req.ADR {
return resp, nil
}
// Get the enabled uplink data-rates to find out which (if any) LR-FHSS data-rates
// are enabled.
band := band.Band()
// Get current DR info.
dr, err := band.GetDataRate(req.DR)
if err != nil {
return resp, err
}
// If we are already at the highest LR-FHSS data-rate, there is nothing to do.
// Note that we only differentiate between coding-rate. The OCW doesn't change
// the speed.
if dr.Modulation == loraband.LRFHSSModulation && dr.CodingRate == "4/6" {
return resp, nil
}
// Get median RSSI.
medRSSI := getMedian(req.UplinkHistory)
// If the median RSSI is below -130, coding-rate 1/3 is recommended,
// if we are on this coding-rate already, there is nothing to do.
if medRSSI < -130 && dr.Modulation == loraband.LRFHSSModulation && dr.CodingRate == "1/3" {
return resp, nil
}
// Find out which LR-FHSS data-rates are enabled (note that not all
// LR-FHSS data-rates might be configured in the channel-plan).
enabledDRs := band.GetEnabledUplinkDataRates()
lrFHSSDataRates := make(map[int]loraband.DataRate)
for _, i := range enabledDRs {
dr, err := band.GetDataRate(i)
if err != nil {
return resp, err
}
// Only get the LR-FHSS data-rates that <= MaxDR
if i <= req.MaxDR && dr.Modulation == loraband.LRFHSSModulation {
lrFHSSDataRates[i] = dr
}
}
// There are no LR-FHSS data-rates enabled, so there is nothing to adjust.
if len(lrFHSSDataRates) == 0 {
return resp, nil
}
// Now we decide which DRs we can use.
drs := make([]int, 0)
// Select LR-FHSS data-rate with coding-rate 4/6 (if any available).
// Note: that for RSSI (median) < -130, coding-rate 1/3 is recommended.
// As the median is taken from the uplink history, make sure that we
// take the median from a full history table.
if medRSSI >= -130 && len(req.UplinkHistory) == 20 {
for k, v := range lrFHSSDataRates {
if v.CodingRate == "4/6" {
drs = append(drs, k)
}
}
}
// This either means coding-rate 1/3 must be used, or no data-rate with
// coding-rate 3/6 is enabled, and thus 1/3 is the only option.
if len(drs) == 0 {
for k, v := range lrFHSSDataRates {
if v.CodingRate == "1/3" {
drs = append(drs, k)
}
}
}
// Sanity check
if len(drs) == 0 {
return resp, nil
}
// Randomly select one of the available LR-FHSS data-rates.
// In case there are multiple with the same coding-rate, we take
// a random one.
s := rand.NewSource(time.Now().Unix())
r := rand.New(s) // initialize local pseudorandom generator
resp.DR = drs[r.Intn(len(drs))]
resp.NbTrans = 1 // 1 is the recommended value
resp.TxPowerIndex = 0 // for now this ADR algorithm only controls the DR
return resp, nil
}
func getMedian(upMetaData []adr.UplinkMetaData) int {
// This should never occur.
if len(upMetaData) == 0 {
return 0
}
rssi := make([]int, 0, len(upMetaData))
for _, up := range upMetaData {
rssi = append(rssi, int(up.MaxRSSI))
}
sort.Ints(rssi)
m := len(rssi) / 2
// Odd
if len(rssi)%2 != 0 {
return rssi[m]
}
return (rssi[m-1] + rssi[m]) / 2
}
package adr
import (
"testing"
"github.com/brocaar/chirpstack-network-server/v3/adr"
"github.com/brocaar/chirpstack-network-server/v3/internal/band"
"github.com/brocaar/chirpstack-network-server/v3/internal/test"
"github.com/stretchr/testify/require"
)
func TestLRFHSSHandler(t *testing.T) {
h := &LRFHSSHandler{}
t.Run("ID", func(t *testing.T) {
assert := require.New(t)
id, err := h.ID()
assert.NoError(err)
assert.Equal("lr_fhss", id)
})
t.Run("Handle without LR-FHSS enabled", func(t *testing.T) {
assert := require.New(t)
conf := test.GetConfig()
band.Setup(conf)
resp, err := h.Handle(adr.HandleRequest{
ADR: true,
DR: 1,
MaxDR: 11,
})
assert.NoError(err)
assert.Equal(adr.HandleResponse{
DR: 1,
}, resp)
})
t.Run("Handle", func(t *testing.T) {
conf := test.GetConfig()
// Add channel with LR-FHSS data-rate enabled.
conf.NetworkServer.NetworkSettings.ExtraChannels = append(conf.NetworkServer.NetworkSettings.ExtraChannels, struct {
Frequency uint32 `mapstructure:"frequency"`
MinDR int `mapstructure:"min_dr"`
MaxDR int `mapstructure:"max_dr"`
}{
Frequency: 867300000,
MinDR: 10,
MaxDR: 11,
})
band.Setup(conf)
fullHistory := make([]adr.UplinkMetaData, 0, 20)
for i := 0; i < 20; i++ {
fullHistory = append(fullHistory, adr.UplinkMetaData{
MaxRSSI: -130,
})
}
tests := []struct {
name string
request adr.HandleRequest
response adr.HandleResponse
}{
{
name: "adr disabled",
request: adr.HandleRequest{
ADR: false,
DR: 0,
NbTrans: 3,
MaxDR: 11,
UplinkHistory: []adr.UplinkMetaData{
{
MaxRSSI: -130,
},
},
},
response: adr.HandleResponse{
DR: 0,
NbTrans: 3,
},
},
{
name: "max dr. prevents lr-fhss",
request: adr.HandleRequest{
ADR: false,
DR: 0,
NbTrans: 3,
MaxDR: 5,
UplinkHistory: []adr.UplinkMetaData{
{
MaxRSSI: -130,
},
},
},
response: adr.HandleResponse{
DR: 0,
NbTrans: 3,
},
},
{
name: "switch to dr 10",
request: adr.HandleRequest{
ADR: true,
DR: 0,
NbTrans: 3,
MaxDR: 11,
UplinkHistory: []adr.UplinkMetaData{
{
MaxRSSI: -130,
},
},
},
response: adr.HandleResponse{
DR: 10,
TxPowerIndex: 0,
NbTrans: 1,
},
},
{
name: "switch to dr 11",
request: adr.HandleRequest{
ADR: true,
DR: 0,
NbTrans: 3,
MaxDR: 11,
UplinkHistory: fullHistory,
},
response: adr.HandleResponse{
DR: 11,
TxPowerIndex: 0,
NbTrans: 1,
},
},
}
for _, tst := range tests {
t.Run(tst.name, func(t *testing.T) {
assert := require.New(t)
resp, err := h.Handle(tst.request)
assert.NoError(err)
assert.Equal(tst.response, resp)
})
}
})
}
......@@ -1095,7 +1095,15 @@ func (ts *NetworkServerAPITestSuite) TestADR() {
AdrAlgorithms: []*ns.ADRAlgorithm{
{
Id: "default",
Name: "Default ADR algorithm",
Name: "Default ADR algorithm (LoRa only)",
},
{
Id: "lora_lr_fhss",
Name: "LoRa & LR-FHSS ADR algorithm",
},
{
Id: "lr_fhss",
Name: "LR-FHSS only ADR algorithm",
},
},
}, resp)
......
......@@ -20,8 +20,8 @@ func Setup(c config.Config) error {
if err != nil {
return errors.Wrap(err, "get band config error")
}
for _, c := range config.C.NetworkServer.NetworkSettings.ExtraChannels {
if err := bandConfig.AddChannel(c.Frequency, c.MinDR, c.MaxDR); err != nil {