Skip to content

Commit 2eeb1d7

Browse files
committed
Implement periodic timesync for Basics Station backend.
1 parent 44d2fb8 commit 2eeb1d7

File tree

7 files changed

+157
-19
lines changed

7 files changed

+157
-19
lines changed

cmd/chirpstack-gateway-bridge/cmd/configfile.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ type="{{ .Backend.Type }}"
133133
# Ping interval.
134134
ping_interval="{{ .Backend.BasicStation.PingInterval }}"
135135
136+
# Timesync interval.
137+
#
138+
# This defines the interval in which the ChirpStack Gateway Bridge sends
139+
# a timesync request to the gateway. Setting this to 0 disables sending
140+
# timesync requests.
141+
timesync_interval="{{ .Backend.BasicStation.TimesyncInterval }}"
142+
136143
# Read timeout.
137144
#
138145
# This interval must be greater than the configured ping interval.

cmd/chirpstack-gateway-bridge/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func init() {
4747
viper.SetDefault("backend.basic_station.bind", ":3001")
4848
viper.SetDefault("backend.basic_station.stats_interval", time.Second*30)
4949
viper.SetDefault("backend.basic_station.ping_interval", time.Minute)
50+
viper.SetDefault("backend.basic_station.timesync_interval", time.Hour)
5051
viper.SetDefault("backend.basic_station.read_timeout", time.Minute+(5*time.Second))
5152
viper.SetDefault("backend.basic_station.write_timeout", time.Second)
5253
viper.SetDefault("backend.basic_station.region", "EU868")

internal/backend/basicstation/backend.go

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ type Backend struct {
5252
scheme string
5353
isClosed bool
5454

55-
statsInterval time.Duration
56-
pingInterval time.Duration
57-
readTimeout time.Duration
58-
writeTimeout time.Duration
55+
statsInterval time.Duration
56+
pingInterval time.Duration
57+
timesyncInterval time.Duration
58+
readTimeout time.Duration
59+
writeTimeout time.Duration
5960

6061
gateways gateways
6162

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

92-
statsInterval: conf.Backend.BasicStation.StatsInterval,
93-
pingInterval: conf.Backend.BasicStation.PingInterval,
94-
readTimeout: conf.Backend.BasicStation.ReadTimeout,
95-
writeTimeout: conf.Backend.BasicStation.WriteTimeout,
93+
statsInterval: conf.Backend.BasicStation.StatsInterval,
94+
pingInterval: conf.Backend.BasicStation.PingInterval,
95+
timesyncInterval: conf.Backend.BasicStation.TimesyncInterval,
96+
readTimeout: conf.Backend.BasicStation.ReadTimeout,
97+
writeTimeout: conf.Backend.BasicStation.WriteTimeout,
9698

9799
region: band.Name(conf.Backend.BasicStation.Region),
98100
frequencyMin: conf.Backend.BasicStation.FrequencyMin,
@@ -504,6 +506,7 @@ func (b *Backend) handleGateway(r *http.Request, conn *connection) {
504506
continue
505507
}
506508
b.handleUplinkDataFrame(gatewayID, pl)
509+
b.sendTimesyncRequest(gatewayID, pl.RadioMetaData.UpInfo)
507510
case structs.JoinRequestMessage:
508511
// handle join-request
509512
var pl structs.JoinRequest
@@ -516,6 +519,7 @@ func (b *Backend) handleGateway(r *http.Request, conn *connection) {
516519
continue
517520
}
518521
b.handleJoinRequest(gatewayID, pl)
522+
b.sendTimesyncRequest(gatewayID, pl.RadioMetaData.UpInfo)
519523
case structs.ProprietaryDataFrameMessage:
520524
// handle proprietary uplink
521525
var pl structs.UplinkProprietaryFrame
@@ -528,6 +532,7 @@ func (b *Backend) handleGateway(r *http.Request, conn *connection) {
528532
continue
529533
}
530534
b.handleProprietaryDataFrame(gatewayID, pl)
535+
b.sendTimesyncRequest(gatewayID, pl.RadioMetaData.UpInfo)
531536
case structs.DownlinkTransmittedMessage:
532537
// handle downlink transmitted
533538
var pl structs.DownlinkTransmitted
@@ -752,7 +757,7 @@ func (b *Backend) handleTimeSync(gatewayID lorawan.EUI64, v structs.TimeSyncRequ
752757
"gateway_id": gatewayID,
753758
"txtime": resp.TxTime,
754759
"gpstime": resp.GPSTime,
755-
}).Info("backend/basicstation: timesync message sent to gateway")
760+
}).Info("backend/basicstation: timesync response sent to gateway")
756761
}
757762

758763
func (b *Backend) sendToGateway(gatewayID lorawan.EUI64, v interface{}) error {
@@ -843,3 +848,48 @@ func (b *Backend) websocketWrap(handler func(*http.Request, *connection), w http
843848
handler(r, &c)
844849
done <- struct{}{}
845850
}
851+
852+
func (b *Backend) sendTimesyncRequest(gatewayID lorawan.EUI64, upInfo structs.RadioMetaDataUpInfo) {
853+
// Nothing to do
854+
if b.timesyncInterval == 0 {
855+
return
856+
}
857+
858+
lastTimesync, err := b.gateways.getLastTimesync(gatewayID)
859+
if err != nil {
860+
log.WithError(err).WithFields(log.Fields{
861+
"gateway_id": gatewayID,
862+
}).Error("backend/basicstation: get last timesync timestamp error")
863+
return
864+
}
865+
866+
// Interval has not been reached yet
867+
if lastTimesync.Add(b.timesyncInterval).After(time.Now()) {
868+
return
869+
}
870+
871+
// Set last timesync
872+
if err := b.gateways.setLastTimesync(gatewayID, time.Now()); err != nil {
873+
log.WithError(err).WithFields(log.Fields{
874+
"gateway_id": gatewayID,
875+
}).Error("backend/basicstation: set last timesync timestamp error")
876+
return
877+
}
878+
879+
timesync := structs.TimeSyncGPSTimeTransfer{
880+
MessageType: structs.TimeSyncMessage,
881+
XTime: upInfo.XTime,
882+
GPSTime: int64(gps.Time(time.Now()).TimeSinceGPSEpoch() / time.Microsecond),
883+
}
884+
885+
if err := b.sendToGateway(gatewayID, &timesync); err != nil {
886+
log.WithError(err).Error("backend/basicstation: send to gateway error")
887+
return
888+
}
889+
890+
log.WithFields(log.Fields{
891+
"gateway_id": gatewayID,
892+
"xtime": timesync.XTime,
893+
"gpstime": timesync.GPSTime,
894+
}).Info("backend/basicstation: timesync request sent to gateway")
895+
}

internal/backend/basicstation/backend_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,48 @@ func (ts *BackendTestSuite) TestProprietaryDataFrame() {
378378
}, &stats))
379379
}
380380

381+
func (ts *BackendTestSuite) TestRequestTimesync() {
382+
assert := require.New(ts.T())
383+
ts.backend.timesyncInterval = time.Hour
384+
gatewayID := lorawan.EUI64{1, 2, 3, 4, 5, 6, 7, 8}
385+
386+
lastTimesyncBefore, err := ts.backend.gateways.getLastTimesync(gatewayID)
387+
assert.NoError(err)
388+
389+
upf := structs.UplinkDataFrame{
390+
RadioMetaData: structs.RadioMetaData{
391+
DR: 5,
392+
Frequency: 868100000,
393+
UpInfo: structs.RadioMetaDataUpInfo{
394+
RCtx: 1,
395+
XTime: 2,
396+
RSSI: 120,
397+
SNR: 5.5,
398+
},
399+
},
400+
MessageType: structs.UplinkDataFrameMessage,
401+
MHDR: 0x40, // unconfirmed data-up
402+
DevAddr: -10,
403+
FCtrl: 0x80, // ADR
404+
FCnt: 400,
405+
FOpts: "0102", // invalid, but for the purpose of testing
406+
MIC: -20,
407+
FPort: -1,
408+
}
409+
assert.NoError(ts.wsClient.WriteJSON(upf))
410+
411+
var timesyncReq structs.TimeSyncGPSTimeTransfer
412+
assert.NoError(ts.wsClient.ReadJSON(&timesyncReq))
413+
414+
assert.EqualValues(timesyncReq.XTime, 2)
415+
assert.True(timesyncReq.GPSTime > 0)
416+
417+
lastTimesyncAfter, err := ts.backend.gateways.getLastTimesync(gatewayID)
418+
assert.NoError(err)
419+
420+
assert.NotEqual(lastTimesyncBefore, lastTimesyncAfter)
421+
}
422+
381423
func (ts *BackendTestSuite) TestDownlinkTransmitted() {
382424
assert := require.New(ts.T())
383425
id, err := uuid.NewV4()

internal/backend/basicstation/gateway.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package basicstation
33
import (
44
"errors"
55
"sync"
6+
"time"
67

78
"github.com/gorilla/websocket"
89

@@ -17,8 +18,9 @@ var (
1718

1819
type connection struct {
1920
sync.Mutex
20-
conn *websocket.Conn
21-
stats *stats.Collector
21+
conn *websocket.Conn
22+
stats *stats.Collector
23+
lastTimesync time.Time
2224
}
2325

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

57+
func (g *gateways) getLastTimesync(id lorawan.EUI64) (time.Time, error) {
58+
g.RLock()
59+
defer g.RUnlock()
60+
61+
gw, ok := g.gateways[id]
62+
if !ok {
63+
return time.Time{}, errGatewayDoesNotExist
64+
}
65+
66+
return gw.lastTimesync, nil
67+
}
68+
69+
func (g *gateways) setLastTimesync(id lorawan.EUI64, ts time.Time) error {
70+
g.Lock()
71+
defer g.Unlock()
72+
73+
gw, ok := g.gateways[id]
74+
if !ok {
75+
return errGatewayDoesNotExist
76+
}
77+
78+
gw.lastTimesync = ts
79+
g.gateways[id] = gw
80+
81+
return nil
82+
}
83+
5584
func (g *gateways) remove(id lorawan.EUI64) error {
5685
g.Lock()
5786
defer g.Unlock()

internal/backend/basicstation/structs/time_sync.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ type TimeSyncResponse struct {
1212
TxTime int64 `json:"txtime"`
1313
GPSTime int64 `json:"gpstime"`
1414
}
15+
16+
// TimeSyncGPSTimeTransfer implements the GPS time transfer
17+
// that is initiated by the NS.
18+
type TimeSyncGPSTimeTransfer struct {
19+
MessageType MessageType `json:"msgtype"`
20+
XTime uint64 `json:"xtime"`
21+
GPSTime int64 `json:"gpstime"`
22+
}

internal/config/config.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ type Config struct {
2626
} `mapstructure:"semtech_udp"`
2727

2828
BasicStation struct {
29-
Bind string `mapstructure:"bind"`
30-
TLSCert string `mapstructure:"tls_cert"`
31-
TLSKey string `mapstructure:"tls_key"`
32-
CACert string `mapstructure:"ca_cert"`
33-
StatsInterval time.Duration `mapstructure:"stats_interval"`
34-
PingInterval time.Duration `mapstructure:"ping_interval"`
35-
ReadTimeout time.Duration `mapstructure:"read_timeout"`
36-
WriteTimeout time.Duration `mapstructure:"write_timeout"`
29+
Bind string `mapstructure:"bind"`
30+
TLSCert string `mapstructure:"tls_cert"`
31+
TLSKey string `mapstructure:"tls_key"`
32+
CACert string `mapstructure:"ca_cert"`
33+
StatsInterval time.Duration `mapstructure:"stats_interval"`
34+
PingInterval time.Duration `mapstructure:"ping_interval"`
35+
TimesyncInterval time.Duration `mapstructure:"timesync_interval"`
36+
ReadTimeout time.Duration `mapstructure:"read_timeout"`
37+
WriteTimeout time.Duration `mapstructure:"write_timeout"`
3738
// TODO: remove Filters in the next major release, use global filters instead
3839
Filters struct {
3940
NetIDs []string `mapstructure:"net_ids"`

0 commit comments

Comments
 (0)