Skip to content

Commit 485fbe8

Browse files
committed
Implement TX ACK and RX time / tmms field.
Closes #19.
1 parent 0e010e5 commit 485fbe8

File tree

11 files changed

+214
-72
lines changed

11 files changed

+214
-72
lines changed

backend/mqttpubsub/backend.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ package mqttpubsub
33
import (
44
"crypto/tls"
55
"crypto/x509"
6-
"io/ioutil"
76
"encoding/json"
87
"fmt"
8+
"io/ioutil"
99
"sync"
1010
"time"
1111

12-
log "github.com/sirupsen/logrus"
1312
"github.com/brocaar/loraserver/api/gw"
1413
"github.com/brocaar/lorawan"
1514
"github.com/eclipse/paho.mqtt.golang"
15+
log "github.com/sirupsen/logrus"
1616
)
1717

1818
// Backend implements a MQTT pub-sub backend.
@@ -125,6 +125,12 @@ func (b *Backend) PublishGatewayStats(mac lorawan.EUI64, stats gw.GatewayStatsPa
125125
return b.publish(topic, stats)
126126
}
127127

128+
// PublishGatewayTXAck publishes a TX ack to the MQTT broker.
129+
func (b *Backend) PublishGatewayTXAck(mac lorawan.EUI64, ack gw.TXAck) error {
130+
topic := fmt.Sprintf("gateway/%s/ack", mac.String())
131+
return b.publish(topic, ack)
132+
}
133+
128134
func (b *Backend) publish(topic string, v interface{}) error {
129135
bytes, err := json.Marshal(v)
130136
if err != nil {

backend/mqttpubsub/backend_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ func TestBackend(t *testing.T) {
3939
So(token.Error(), ShouldBeNil)
4040

4141
Convey("When publishing a RXPacket", func() {
42+
now := time.Now().UTC()
4243
rxPacket := gw.RXPacketBytes{
4344
RXInfo: gw.RXInfo{
4445
MAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
45-
Time: time.Now().UTC(),
46+
Time: &now,
4647
},
4748
PHYPayload: []byte{1, 2, 3, 4},
4849
}

cmd/lora-gateway-bridge/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"syscall"
99
"time"
1010

11-
log "github.com/sirupsen/logrus"
1211
"github.com/brocaar/lora-gateway-bridge/backend/mqttpubsub"
1312
"github.com/brocaar/lora-gateway-bridge/gateway"
1413
"github.com/brocaar/lorawan"
1514
"github.com/codegangsta/cli"
15+
log "github.com/sirupsen/logrus"
1616
)
1717

1818
var version string // set by the compiler
@@ -76,6 +76,14 @@ func run(c *cli.Context) error {
7676
}
7777
}()
7878

79+
go func() {
80+
for txAck := range gw.TXAckChan() {
81+
if err := pubsub.PublishGatewayTXAck(txAck.MAC, txAck); err != nil {
82+
log.Errorf("could not publish TXAck: %s", err)
83+
}
84+
}
85+
}()
86+
7987
sigChan := make(chan os.Signal)
8088
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
8189
log.WithField("signal", <-sigChan).Info("signal received")

docs/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ googleAnalytics = "UA-3512995-9"
3232
weight = 4
3333

3434
[params]
35-
version = "2.1.6"
35+
version = "2.2.0"

docs/content/overview/changelog.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ menu:
88

99
## Changelog
1010

11+
### 2.2.0
12+
13+
**Features:**
14+
15+
* LoRa Gateway Bridge now publishes TX acknowledgement messages over MQTT.
16+
See [MQTT topics](https://docs.loraserver.io/lora-gateway-bridge/use/data/).
17+
18+
* TX (GPS) time field is now implemented to transmit at given timestamp
19+
(only possible when the gateway has a GPS time-source).
20+
21+
**Bugfixes:**
22+
23+
* Without GPS time-source, the gateway would use `0001-01-01T00:00:00Z`
24+
as RX `time`. The `time` field is now omitted when unavailable.
25+
26+
1127
### 2.1.6
1228

1329
**Features:**
@@ -28,7 +44,6 @@ menu:
2844
re-connect the protocol version would be downgraded
2945
([paho.mqtt.golang#116](https://github.com/eclipse/paho.mqtt.golang/issues/116)).
3046

31-
3247
### 2.1.4
3348

3449
* Retry connecting to MQTT broker on startup (thanks @jcampanell-cablelabs).

docs/content/use/data.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Topic for received packets (from nodes). Example payload:
6464
"rfChain": 1,
6565
"rssi": -57,
6666
"size": 23,
67-
"time": "0001-01-01T00:00:00Z",
67+
"time": "2017-12-11T10:14:54.619571Z", // timestamp (only set when the gateway has a GPS time-source)
6868
"timestamp": 2074240683 // gateway internal timestamp (32 bit) with microsecond precision
6969
}
7070
}
@@ -77,6 +77,7 @@ Example payload:
7777

7878
```json
7979
{
80+
"token": 65535, // random token (uint16), used for acknowledgements
8081
"phyPayload": "IKu70cumKom7BREUFrxlHtM=",
8182
"txInfo": {
8283
"board": 0,
@@ -91,11 +92,37 @@ Example payload:
9192
"immediately": false,
9293
"mac": "1dee08d0b691d149",
9394
"power": 14,
94-
"timestamp": 2079240683
95+
"timestamp": 2079240683, // gateway internal timestamp for transmission -OR-
96+
"time": "2017-12-11T10:14:54.619571Z" // timestamp for transmission (only when the gateway has a GPS time-source)
9597
}
9698
}
9799
```
98100

101+
When `immediately` is set to `false`, either the `timestamp` **or** the
102+
`time` field must be present to tell the gateway at what (internal) time
103+
the frame must be transmitted.
104+
99105
Optionally, the field `iPol` (type `bool`) can be used to control the
100106
LoRa modulation polarization inversion. When left blank (`null`), the default
101-
will be used (which is `true` for downlink LoRa modulation.
107+
will be used (which is `true` for downlink LoRa modulation.
108+
109+
### gateway/[mac]/ack
110+
111+
Topic for received TX acknowledgements (or TX errors). Example payload:
112+
113+
```json
114+
{
115+
"token": 65535, // same token used during transmission
116+
"error": "COLLISION_PACKET" // not set in case of acknowledgement
117+
}
118+
```
119+
120+
Possible error values are:
121+
122+
* `TOO_LATE`: Rejected because it was already too late to program this packet for downlink
123+
* `TOO_EARLY`: Rejected because downlink packet timestamp is too much in advance
124+
* `COLLISION_PACKET`: Rejected because there was already a packet programmed in requested timeframe
125+
* `COLLISION_BEACON`: Rejected because there was already a beacon planned in requested timeframe
126+
* `TX_FREQ`: Rejected because requested frequency is not supported by TX RF chain
127+
* `TX_POWER`: Rejected because requested power is not supported by gateway
128+
* `GPS_UNLOCKED`: Rejected because GPS is unlocked, so GPS timestamp cannot be used

gateway/backend.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func (c *gateways) cleanup() error {
8181
// Backend implements a Semtech gateway backend.
8282
type Backend struct {
8383
conn *net.UDPConn
84+
txAckChan chan gw.TXAck
8485
rxChan chan gw.RXPacketBytes
8586
statsChan chan gw.GatewayStatsPacket
8687
udpSendChan chan udpPacket
@@ -105,6 +106,7 @@ func NewBackend(bind string, onNew func(lorawan.EUI64) error, onDelete func(lora
105106
b := &Backend{
106107
skipCRCCheck: skipCRCCheck,
107108
conn: conn,
109+
txAckChan: make(chan gw.TXAck),
108110
rxChan: make(chan gw.RXPacketBytes),
109111
statsChan: make(chan gw.GatewayStatsPacket),
110112
udpSendChan: make(chan udpPacket),
@@ -163,11 +165,17 @@ func (b *Backend) RXPacketChan() chan gw.RXPacketBytes {
163165
return b.rxChan
164166
}
165167

166-
// StatsChan returns the channel containg the received gateway stats.
168+
// StatsChan returns the channel containing the received gateway stats.
167169
func (b *Backend) StatsChan() chan gw.GatewayStatsPacket {
168170
return b.statsChan
169171
}
170172

173+
// TXAckChan returns the channel containing the TX acknowledgements
174+
// (or errors).
175+
func (b *Backend) TXAckChan() chan gw.TXAck {
176+
return b.txAckChan
177+
}
178+
171179
// Send sends the given packet to the gateway.
172180
func (b *Backend) Send(txPacket gw.TXPacketBytes) error {
173181
gw, err := b.gateways.get(txPacket.TXInfo.MAC)
@@ -179,6 +187,7 @@ func (b *Backend) Send(txPacket gw.TXPacketBytes) error {
179187
return err
180188
}
181189
pullResp := PullRespPacket{
190+
RandomToken: txPacket.Token,
182191
ProtocolVersion: gw.protocolVersion,
183192
Payload: PullRespPayload{
184193
TXPK: txpk,
@@ -365,23 +374,10 @@ func (b *Backend) handleTXACK(addr *net.UDPAddr, data []byte) error {
365374
if err := p.UnmarshalBinary(data); err != nil {
366375
return err
367376
}
368-
var errBool bool
369377

370-
logFields := log.Fields{
371-
"mac": p.GatewayMAC,
372-
"random_token": p.RandomToken,
373-
}
374378
if p.Payload != nil {
375-
if p.Payload.TXPKACK.Error != "NONE" {
376-
errBool = true
377-
}
378-
logFields["error"] = p.Payload.TXPKACK.Error
379-
}
380-
381-
if errBool {
382-
log.WithFields(logFields).Error("gateway: tx ack received")
383-
} else {
384-
log.WithFields(logFields).Info("gateway: tx ack received")
379+
txAck := newTXAckFromTXPKACK(p.GatewayMAC, p.RandomToken, p.Payload.TXPKACK)
380+
b.txAckChan <- txAck
385381
}
386382

387383
return nil
@@ -428,7 +424,6 @@ func newRXPacketsFromRXPK(mac lorawan.EUI64, rxpk RXPK) ([]gw.RXPacketBytes, err
428424
PHYPayload: b,
429425
RXInfo: gw.RXInfo{
430426
MAC: mac,
431-
Time: time.Time(rxpk.Time),
432427
Timestamp: rxpk.Tmst,
433428
Frequency: int(rxpk.Freq * 1000000),
434429
Channel: int(rxpk.Chan),
@@ -443,6 +438,11 @@ func newRXPacketsFromRXPK(mac lorawan.EUI64, rxpk RXPK) ([]gw.RXPacketBytes, err
443438
},
444439
}
445440

441+
if rxpk.Time != nil {
442+
ts := time.Time(*rxpk.Time)
443+
rxPacket.RXInfo.Time = &ts
444+
}
445+
446446
if len(rxpk.RSig) == 0 {
447447
rxPackets = append(rxPackets, rxPacket)
448448
}
@@ -476,6 +476,11 @@ func newTXPKFromTXPacket(txPacket gw.TXPacketBytes) (TXPK, error) {
476476
Brd: uint8(txPacket.TXInfo.Board),
477477
}
478478

479+
if txPacket.TXInfo.Time != nil {
480+
ct := CompactTime(*txPacket.TXInfo.Time)
481+
txpk.Tmms = &ct
482+
}
483+
479484
if txPacket.TXInfo.DataRate.Modulation == band.FSKModulation {
480485
txpk.FDev = uint16(txPacket.TXInfo.DataRate.BitRate / 2)
481486
}
@@ -491,6 +496,19 @@ func newTXPKFromTXPacket(txPacket gw.TXPacketBytes) (TXPK, error) {
491496
return txpk, nil
492497
}
493498

499+
func newTXAckFromTXPKACK(mac lorawan.EUI64, token uint16, ack TXPKACK) gw.TXAck {
500+
var err string
501+
if ack.Error != "NONE" {
502+
err = ack.Error
503+
}
504+
505+
return gw.TXAck{
506+
MAC: mac,
507+
Token: token,
508+
Error: err,
509+
}
510+
}
511+
494512
func newDataRateFromDatR(d DatR) (band.DataRate, error) {
495513
var dr band.DataRate
496514

0 commit comments

Comments
 (0)