Skip to content

Commit

Permalink
Implement periodic timesync for Basics Station backend.
Browse files Browse the repository at this point in the history
  • Loading branch information
brocaar committed Jun 13, 2022
1 parent 44d2fb8 commit 2eeb1d7
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 19 deletions.
7 changes: 7 additions & 0 deletions cmd/chirpstack-gateway-bridge/cmd/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ type="{{ .Backend.Type }}"
# Ping interval.
ping_interval="{{ .Backend.BasicStation.PingInterval }}"
# Timesync interval.
#
# This defines the interval in which the ChirpStack Gateway Bridge sends
# a timesync request to the gateway. Setting this to 0 disables sending
# timesync requests.
timesync_interval="{{ .Backend.BasicStation.TimesyncInterval }}"
# Read timeout.
#
# This interval must be greater than the configured ping interval.
Expand Down
1 change: 1 addition & 0 deletions cmd/chirpstack-gateway-bridge/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func init() {
viper.SetDefault("backend.basic_station.bind", ":3001")
viper.SetDefault("backend.basic_station.stats_interval", time.Second*30)
viper.SetDefault("backend.basic_station.ping_interval", time.Minute)
viper.SetDefault("backend.basic_station.timesync_interval", time.Hour)
viper.SetDefault("backend.basic_station.read_timeout", time.Minute+(5*time.Second))
viper.SetDefault("backend.basic_station.write_timeout", time.Second)
viper.SetDefault("backend.basic_station.region", "EU868")
Expand Down
68 changes: 59 additions & 9 deletions internal/backend/basicstation/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ type Backend struct {
scheme string
isClosed bool

statsInterval time.Duration
pingInterval time.Duration
readTimeout time.Duration
writeTimeout time.Duration
statsInterval time.Duration
pingInterval time.Duration
timesyncInterval time.Duration
readTimeout time.Duration
writeTimeout time.Duration

gateways gateways

Expand Down Expand Up @@ -89,10 +90,11 @@ func NewBackend(conf config.Config) (*Backend, error) {
tlsCert: conf.Backend.BasicStation.TLSCert,
tlsKey: conf.Backend.BasicStation.TLSKey,

statsInterval: conf.Backend.BasicStation.StatsInterval,
pingInterval: conf.Backend.BasicStation.PingInterval,
readTimeout: conf.Backend.BasicStation.ReadTimeout,
writeTimeout: conf.Backend.BasicStation.WriteTimeout,
statsInterval: conf.Backend.BasicStation.StatsInterval,
pingInterval: conf.Backend.BasicStation.PingInterval,
timesyncInterval: conf.Backend.BasicStation.TimesyncInterval,
readTimeout: conf.Backend.BasicStation.ReadTimeout,
writeTimeout: conf.Backend.BasicStation.WriteTimeout,

region: band.Name(conf.Backend.BasicStation.Region),
frequencyMin: conf.Backend.BasicStation.FrequencyMin,
Expand Down Expand Up @@ -504,6 +506,7 @@ func (b *Backend) handleGateway(r *http.Request, conn *connection) {
continue
}
b.handleUplinkDataFrame(gatewayID, pl)
b.sendTimesyncRequest(gatewayID, pl.RadioMetaData.UpInfo)
case structs.JoinRequestMessage:
// handle join-request
var pl structs.JoinRequest
Expand All @@ -516,6 +519,7 @@ func (b *Backend) handleGateway(r *http.Request, conn *connection) {
continue
}
b.handleJoinRequest(gatewayID, pl)
b.sendTimesyncRequest(gatewayID, pl.RadioMetaData.UpInfo)
case structs.ProprietaryDataFrameMessage:
// handle proprietary uplink
var pl structs.UplinkProprietaryFrame
Expand All @@ -528,6 +532,7 @@ func (b *Backend) handleGateway(r *http.Request, conn *connection) {
continue
}
b.handleProprietaryDataFrame(gatewayID, pl)
b.sendTimesyncRequest(gatewayID, pl.RadioMetaData.UpInfo)
case structs.DownlinkTransmittedMessage:
// handle downlink transmitted
var pl structs.DownlinkTransmitted
Expand Down Expand Up @@ -752,7 +757,7 @@ func (b *Backend) handleTimeSync(gatewayID lorawan.EUI64, v structs.TimeSyncRequ
"gateway_id": gatewayID,
"txtime": resp.TxTime,
"gpstime": resp.GPSTime,
}).Info("backend/basicstation: timesync message sent to gateway")
}).Info("backend/basicstation: timesync response sent to gateway")
}

func (b *Backend) sendToGateway(gatewayID lorawan.EUI64, v interface{}) error {
Expand Down Expand Up @@ -843,3 +848,48 @@ func (b *Backend) websocketWrap(handler func(*http.Request, *connection), w http
handler(r, &c)
done <- struct{}{}
}

func (b *Backend) sendTimesyncRequest(gatewayID lorawan.EUI64, upInfo structs.RadioMetaDataUpInfo) {
// Nothing to do
if b.timesyncInterval == 0 {
return
}

lastTimesync, err := b.gateways.getLastTimesync(gatewayID)
if err != nil {
log.WithError(err).WithFields(log.Fields{
"gateway_id": gatewayID,
}).Error("backend/basicstation: get last timesync timestamp error")
return
}

// Interval has not been reached yet
if lastTimesync.Add(b.timesyncInterval).After(time.Now()) {
return
}

// Set last timesync
if err := b.gateways.setLastTimesync(gatewayID, time.Now()); err != nil {
log.WithError(err).WithFields(log.Fields{
"gateway_id": gatewayID,
}).Error("backend/basicstation: set last timesync timestamp error")
return
}

timesync := structs.TimeSyncGPSTimeTransfer{
MessageType: structs.TimeSyncMessage,
XTime: upInfo.XTime,
GPSTime: int64(gps.Time(time.Now()).TimeSinceGPSEpoch() / time.Microsecond),
}

if err := b.sendToGateway(gatewayID, &timesync); err != nil {
log.WithError(err).Error("backend/basicstation: send to gateway error")
return
}

log.WithFields(log.Fields{
"gateway_id": gatewayID,
"xtime": timesync.XTime,
"gpstime": timesync.GPSTime,
}).Info("backend/basicstation: timesync request sent to gateway")
}
42 changes: 42 additions & 0 deletions internal/backend/basicstation/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,48 @@ func (ts *BackendTestSuite) TestProprietaryDataFrame() {
}, &stats))
}

func (ts *BackendTestSuite) TestRequestTimesync() {
assert := require.New(ts.T())
ts.backend.timesyncInterval = time.Hour
gatewayID := lorawan.EUI64{1, 2, 3, 4, 5, 6, 7, 8}

lastTimesyncBefore, err := ts.backend.gateways.getLastTimesync(gatewayID)
assert.NoError(err)

upf := structs.UplinkDataFrame{
RadioMetaData: structs.RadioMetaData{
DR: 5,
Frequency: 868100000,
UpInfo: structs.RadioMetaDataUpInfo{
RCtx: 1,
XTime: 2,
RSSI: 120,
SNR: 5.5,
},
},
MessageType: structs.UplinkDataFrameMessage,
MHDR: 0x40, // unconfirmed data-up
DevAddr: -10,
FCtrl: 0x80, // ADR
FCnt: 400,
FOpts: "0102", // invalid, but for the purpose of testing
MIC: -20,
FPort: -1,
}
assert.NoError(ts.wsClient.WriteJSON(upf))

var timesyncReq structs.TimeSyncGPSTimeTransfer
assert.NoError(ts.wsClient.ReadJSON(&timesyncReq))

assert.EqualValues(timesyncReq.XTime, 2)
assert.True(timesyncReq.GPSTime > 0)

lastTimesyncAfter, err := ts.backend.gateways.getLastTimesync(gatewayID)
assert.NoError(err)

assert.NotEqual(lastTimesyncBefore, lastTimesyncAfter)
}

func (ts *BackendTestSuite) TestDownlinkTransmitted() {
assert := require.New(ts.T())
id, err := uuid.NewV4()
Expand Down
33 changes: 31 additions & 2 deletions internal/backend/basicstation/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package basicstation
import (
"errors"
"sync"
"time"

"github.com/gorilla/websocket"

Expand All @@ -17,8 +18,9 @@ var (

type connection struct {
sync.Mutex
conn *websocket.Conn
stats *stats.Collector
conn *websocket.Conn
stats *stats.Collector
lastTimesync time.Time
}

type gateways struct {
Expand Down Expand Up @@ -52,6 +54,33 @@ func (g *gateways) set(id lorawan.EUI64, c *connection) error {
return nil
}

func (g *gateways) getLastTimesync(id lorawan.EUI64) (time.Time, error) {
g.RLock()
defer g.RUnlock()

gw, ok := g.gateways[id]
if !ok {
return time.Time{}, errGatewayDoesNotExist
}

return gw.lastTimesync, nil
}

func (g *gateways) setLastTimesync(id lorawan.EUI64, ts time.Time) error {
g.Lock()
defer g.Unlock()

gw, ok := g.gateways[id]
if !ok {
return errGatewayDoesNotExist
}

gw.lastTimesync = ts
g.gateways[id] = gw

return nil
}

func (g *gateways) remove(id lorawan.EUI64) error {
g.Lock()
defer g.Unlock()
Expand Down
8 changes: 8 additions & 0 deletions internal/backend/basicstation/structs/time_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ type TimeSyncResponse struct {
TxTime int64 `json:"txtime"`
GPSTime int64 `json:"gpstime"`
}

// TimeSyncGPSTimeTransfer implements the GPS time transfer
// that is initiated by the NS.
type TimeSyncGPSTimeTransfer struct {
MessageType MessageType `json:"msgtype"`
XTime uint64 `json:"xtime"`
GPSTime int64 `json:"gpstime"`
}
17 changes: 9 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ type Config struct {
} `mapstructure:"semtech_udp"`

BasicStation struct {
Bind string `mapstructure:"bind"`
TLSCert string `mapstructure:"tls_cert"`
TLSKey string `mapstructure:"tls_key"`
CACert string `mapstructure:"ca_cert"`
StatsInterval time.Duration `mapstructure:"stats_interval"`
PingInterval time.Duration `mapstructure:"ping_interval"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
Bind string `mapstructure:"bind"`
TLSCert string `mapstructure:"tls_cert"`
TLSKey string `mapstructure:"tls_key"`
CACert string `mapstructure:"ca_cert"`
StatsInterval time.Duration `mapstructure:"stats_interval"`
PingInterval time.Duration `mapstructure:"ping_interval"`
TimesyncInterval time.Duration `mapstructure:"timesync_interval"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
// TODO: remove Filters in the next major release, use global filters instead
Filters struct {
NetIDs []string `mapstructure:"net_ids"`
Expand Down

0 comments on commit 2eeb1d7

Please sign in to comment.