├── .gitignore ├── LICENSE ├── README.md ├── examples ├── local_conf.json └── usage.go └── gateway ├── band └── band.go ├── base_test.go ├── client.go ├── client_test.go ├── ip.go ├── mac.go ├── mac_test.go ├── packets.go ├── packettype_string.go ├── structs.go └── structs_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | *.iml 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Orne Brocaar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoRa Packet Forwarder Client 2 | 3 | Enables communication with the [LoRa Packet Forarder](https://github.com/Lora-net/packet_forwarder) from Semtech. 4 | 5 | The code is based on the [LoRa Gateway Bridge](https://github.com/brocaar/lora-gateway-bridge) which is also used in the [TTN Packet Forwarder Bridge](https://github.com/TheThingsNetwork/packet-forwarder-bridge) but targets a more generic aproach to talk to the Gateway, e.g. you can receive packets with wrong CRC and there is no dependencie to LoRaWAN specifics like `lorawan.EUI64`. 6 | 7 | 8 | # Usage 9 | 10 | Make sure you have a [LoRa Packet Forarder](https://github.com/Lora-net/packet_forwarder) configured 11 | to communicate with the client endpoint. You can find example configs and code in `/examples`: 12 | [usage.go](https://github.com/Lobaro/lora-packet-forwarder-client/blob/master/examples/usage.go) and 13 | [local_conf.json](https://github.com/Lobaro/lora-packet-forwarder-client/blob/master/examples/local_conf.json) 14 | 15 | ``` 16 | func main() { 17 | client, err := gateway.NewClient( 18 | ":1680", 19 | func(gwMac gateway.Mac) error { 20 | return nil 21 | }, 22 | func(gwMac gateway.Mac) error { 23 | return nil 24 | }, 25 | ) 26 | 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | defer client.Close() 32 | 33 | log.Info("Waiting for gateway packet ...") 34 | msg := <-client.RXPacketChan() 35 | log.Infof("Received packet from Gateway with phy payload %v", msg.PHYPayload) 36 | 37 | log.Info("Exit") 38 | } 39 | ``` 40 | 41 | ## License 42 | 43 | LoRa Gateway Bridge is distributed under the MIT license. See 44 | [LICENSE](https://github.com/Lobaro/lora-packet-forwarder-client/blob/master/LICENSE). 45 | -------------------------------------------------------------------------------- /examples/local_conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "gateway_conf": { 3 | "gateway_ID": "AA555A0000000000", 4 | /* change with default server address/ports, or overwrite in local_conf.json */ 5 | "server_address": "localhost", 6 | "serv_port_up": 1680, 7 | "serv_port_down": 1680, 8 | /* adjust the following parameters for your network */ 9 | "keepalive_interval": 10, 10 | "stat_interval": 30, 11 | "push_timeout_ms": 100, 12 | /* forward only valid packets */ 13 | "forward_crc_valid": true, 14 | "forward_crc_error": false, 15 | "forward_crc_disabled": false 16 | } 17 | } -------------------------------------------------------------------------------- /examples/usage.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "github.com/Lobaro/lora-packet-forwarder-client/gateway" 5 | "github.com/Lobaro/lora-packet-forwarder-client/gateway/band" 6 | "github.com/apex/log" 7 | ) 8 | 9 | var exit = make(chan struct{}) 10 | 11 | func Exit() { 12 | close(exit) 13 | } 14 | 15 | func main() { 16 | client, err := gateway.NewClient(":1680", onNewGateway, onDeleteGateway) 17 | 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | defer client.Close() 23 | 24 | go handleRxPackets(client) 25 | 26 | log.Info("Listening on port :1680. Press Ctrl + c to exit.") 27 | 28 | // Block till Exit() is called or program terminates 29 | <-exit 30 | } 31 | 32 | func onNewGateway(gwMac gateway.Mac) error { 33 | // New gateway with given mac address 34 | return nil 35 | } 36 | 37 | func onDeleteGateway(gwMac gateway.Mac) error { 38 | // Removed gateway with given mac address 39 | return nil 40 | } 41 | 42 | func handleRxPackets(client *gateway.Client) { 43 | for { 44 | select { 45 | case msg := <-client.RXPacketChan(): 46 | log.Infof("Received packet from Gateway with phy payload %v", msg.PHYPayload) 47 | } 48 | } 49 | 50 | } 51 | 52 | // SendDownlinkExample demonstrates how to send down-link data to the gateway 53 | func SendDownlinkExample(client *gateway.Client) { 54 | 55 | txPacket := gateway.TXPacketBytes{ 56 | TXInfo: gateway.TXInfo{ 57 | MAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 58 | Immediately: true, 59 | Timestamp: 12345, 60 | Frequency: 868100000, 61 | Power: 14, 62 | DataRate: band.DataRate{ 63 | Modulation: band.LoRaModulation, 64 | SpreadFactor: 12, 65 | Bandwidth: 250, 66 | }, 67 | CodeRate: "4/5", 68 | }, 69 | PHYPayload: []byte{1, 2, 3, 4}, 70 | } 71 | client.Send(txPacket) 72 | } 73 | 74 | func example() { 75 | client, err := gateway.NewClient( 76 | ":1680", 77 | func(gwMac gateway.Mac) error { 78 | return nil 79 | }, 80 | func(gwMac gateway.Mac) error { 81 | return nil 82 | }, 83 | ) 84 | 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | defer client.Close() 90 | 91 | log.Info("Waiting for gateway packet ...") 92 | msg := <-client.RXPacketChan() 93 | log.Infof("Received packet from Gateway with phy payload %v", msg.PHYPayload) 94 | 95 | log.Info("Exit") 96 | } 97 | -------------------------------------------------------------------------------- /gateway/band/band.go: -------------------------------------------------------------------------------- 1 | // Package band provides band specific defaults and configuration. 2 | package band 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // Name defines the band-name type. 11 | type Name string 12 | 13 | // Modulation defines the modulation type. 14 | type Modulation string 15 | 16 | // Possible modulation types. 17 | const ( 18 | LoRaModulation Modulation = "LORA" 19 | FSKModulation Modulation = "FSK" 20 | ) 21 | 22 | // DataRate defines a data rate 23 | type DataRate struct { 24 | Modulation Modulation `json:"modulation"` 25 | SpreadFactor int `json:"spreadFactor,omitempty"` // used for LoRa 26 | Bandwidth int `json:"bandwidth,omitempty"` // in kHz, used for LoRa 27 | BitRate int `json:"bitRate,omitempty"` // bits per second, used for FSK 28 | } 29 | 30 | // MaxPayloadSize defines the max payload size 31 | type MaxPayloadSize struct { 32 | M int // The maximum MACPayload size length 33 | N int // The maximum application payload length in the absence of the optional FOpt control field 34 | } 35 | 36 | // Channel defines the channel structure 37 | type Channel struct { 38 | Frequency int // frequency in Hz 39 | DataRates []int // each int mapping to an index in DataRateConfiguration 40 | } 41 | 42 | // Band defines an region specific ISM band implementation for LoRa. 43 | type Band struct { 44 | // DefaultTXPower defines the default radiated transmit output power 45 | DefaultTXPower int 46 | 47 | // ImplementsCFlist defines if the band implements the optional channel 48 | // frequency list. 49 | ImplementsCFlist bool 50 | 51 | // RX2Frequency defines the fixed frequency for the RX2 receive window 52 | RX2Frequency int 53 | 54 | // RX2DataRate defines the fixed data-rate for the RX2 receive window 55 | RX2DataRate int 56 | 57 | // MaxFcntGap defines the MAC_FCNT_GAP default value. 58 | MaxFCntGap uint32 59 | 60 | // ADRACKLimit defines the ADR_ACK_LIMIT default value. 61 | ADRACKLimit int 62 | 63 | // ADRACKDelay defines the ADR_ACK_DELAY default value. 64 | ADRACKDelay int 65 | 66 | // ReceiveDelay1 defines the RECEIVE_DELAY1 default value. 67 | ReceiveDelay1 time.Duration 68 | 69 | // ReceiveDelay2 defines the RECEIVE_DELAY2 default value. 70 | ReceiveDelay2 time.Duration 71 | 72 | // JoinAcceptDelay1 defines the JOIN_ACCEPT_DELAY1 default value. 73 | JoinAcceptDelay1 time.Duration 74 | 75 | // JoinAcceptDelay2 defines the JOIN_ACCEPT_DELAY2 default value. 76 | JoinAcceptDelay2 time.Duration 77 | 78 | // ACKTimeoutMin defines the ACK_TIMEOUT min. default value. 79 | ACKTimeoutMin time.Duration 80 | 81 | // ACKTimeoutMax defines the ACK_TIMEOUT max. default value. 82 | ACKTimeoutMax time.Duration 83 | 84 | // DataRates defines the available data rates. 85 | DataRates []DataRate 86 | 87 | // MaxPayloadSize defines the maximum payload size, per data-rate. 88 | MaxPayloadSize []MaxPayloadSize 89 | 90 | // RX1DataRate defines the RX1 data-rate given the uplink data-rate 91 | // and a RX1DROffset value. 92 | RX1DataRate [][]int 93 | 94 | // TXPower defines the TX power configuration. 95 | TXPower []int 96 | 97 | // UplinkChannels defines the list of (default) configured uplink channels. 98 | UplinkChannels []Channel 99 | 100 | // DownlinkChannels defines the list of (default) configured downlink 101 | // channels. 102 | DownlinkChannels []Channel 103 | 104 | // getRX1ChannelFunc implements a function which returns the RX1 channel 105 | // based on the uplink / TX channel. 106 | getRX1ChannelFunc func(txChannel int) int 107 | 108 | // getRX1FrequencyFunc implements a function which returns the RX1 frequency 109 | // given the uplink frequency. 110 | getRX1FrequencyFunc func(band *Band, txFrequency int) (int, error) 111 | } 112 | 113 | // GetRX1Channel returns the channel to use for RX1 given the channel used 114 | // for uplink. 115 | func (b *Band) GetRX1Channel(txChannel int) int { 116 | return b.getRX1ChannelFunc(txChannel) 117 | } 118 | 119 | // GetRX1Frequency returns the frequency to use for RX1 given the uplink 120 | // frequency. 121 | func (b *Band) GetRX1Frequency(txFrequency int) (int, error) { 122 | return b.getRX1FrequencyFunc(b, txFrequency) 123 | } 124 | 125 | // GetDataRate returns the index of the given DataRate. 126 | func (b *Band) GetDataRate(dr DataRate) (int, error) { 127 | for i, d := range b.DataRates { 128 | if d == dr { 129 | return i, nil 130 | } 131 | } 132 | return 0, errors.New("lorawan/band: the given data-rate does not exist") 133 | } 134 | 135 | // GetRX1DataRateForOffset returns the data-rate for the given offset. 136 | func (b *Band) GetRX1DataRateForOffset(dr, drOffset int) (int, error) { 137 | if dr >= len(b.RX1DataRate) { 138 | return 0, fmt.Errorf("lorawan/band: invalid data-rate: %d", dr) 139 | } 140 | 141 | if drOffset >= len(b.RX1DataRate[dr]) { 142 | return 0, fmt.Errorf("lorawan/band: invalid data-rate offset: %d", drOffset) 143 | } 144 | return b.RX1DataRate[dr][drOffset], nil 145 | } 146 | -------------------------------------------------------------------------------- /gateway/base_test.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | log "github.com/Sirupsen/logrus" 5 | ) 6 | 7 | func init() { 8 | log.SetLevel(log.ErrorLevel) 9 | } 10 | -------------------------------------------------------------------------------- /gateway/client.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "regexp" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/Lobaro/lora-packet-forwarder-client/gateway/band" 14 | log "github.com/Sirupsen/logrus" 15 | ) 16 | 17 | var errGatewayDoesNotExist = errors.New("gateway does not exist") 18 | var gatewayCleanupDuration = -1 * time.Minute 19 | var loRaDataRateRegex = regexp.MustCompile(`SF(\d+)BW(\d+)`) 20 | 21 | type udpPacket struct { 22 | addr *net.UDPAddr 23 | data []byte 24 | } 25 | 26 | type gateway struct { 27 | addr *net.UDPAddr 28 | lastSeen time.Time 29 | protocolVersion uint8 30 | } 31 | 32 | type gateways struct { 33 | sync.RWMutex 34 | gateways map[Mac]gateway 35 | onNew func(Mac) error 36 | onDelete func(Mac) error 37 | } 38 | 39 | func (c *gateways) get(mac Mac) (gateway, error) { 40 | defer c.RUnlock() 41 | c.RLock() 42 | gw, ok := c.gateways[mac] 43 | if !ok { 44 | return gw, errGatewayDoesNotExist 45 | } 46 | return gw, nil 47 | } 48 | 49 | func (c *gateways) set(mac Mac, gw gateway) error { 50 | defer c.Unlock() 51 | c.Lock() 52 | _, ok := c.gateways[mac] 53 | if !ok && c.onNew != nil { 54 | if err := c.onNew(mac); err != nil { 55 | return err 56 | } 57 | } 58 | c.gateways[mac] = gw 59 | return nil 60 | } 61 | 62 | func (c *gateways) cleanup() error { 63 | defer c.Unlock() 64 | c.Lock() 65 | for mac := range c.gateways { 66 | if c.gateways[mac].lastSeen.Before(time.Now().Add(gatewayCleanupDuration)) { 67 | if c.onDelete != nil { 68 | if err := c.onDelete(mac); err != nil { 69 | return err 70 | } 71 | } 72 | delete(c.gateways, mac) 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | // Client implements a Semtech gateway client/backend. 79 | type Client struct { 80 | CheckCrc bool 81 | 82 | log *log.Logger 83 | conn *net.UDPConn 84 | rxChan chan RXPacketBytes 85 | statsChan chan GatewayStatsPacket 86 | udpSendChan chan udpPacket 87 | closed bool 88 | gateways gateways 89 | wg sync.WaitGroup 90 | } 91 | 92 | // NewClient creates a new Client. 93 | func NewClient(bind string, onNew func(Mac) error, onDelete func(Mac) error) (*Client, error) { 94 | addr, err := net.ResolveUDPAddr("udp", bind) 95 | if err != nil { 96 | return nil, err 97 | } 98 | log.WithField("addr", addr).Info("gateway: starting gateway udp listener") 99 | conn, err := net.ListenUDP("udp", addr) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | c := &Client{ 105 | CheckCrc: true, 106 | conn: conn, 107 | rxChan: make(chan RXPacketBytes), 108 | statsChan: make(chan GatewayStatsPacket), 109 | udpSendChan: make(chan udpPacket), 110 | gateways: gateways{ 111 | gateways: make(map[Mac]gateway), 112 | onNew: onNew, 113 | onDelete: onDelete, 114 | }, 115 | } 116 | 117 | go func() { 118 | for { 119 | if err := c.gateways.cleanup(); err != nil { 120 | c.log.Errorf("gateway: gateways cleanup failed: %s", err) 121 | } 122 | time.Sleep(time.Minute) 123 | } 124 | }() 125 | 126 | go func() { 127 | c.wg.Add(1) 128 | err := c.readPackets() 129 | if !c.closed { 130 | c.log.Fatal(err) 131 | } 132 | c.wg.Done() 133 | }() 134 | 135 | go func() { 136 | c.wg.Add(1) 137 | err := c.sendPackets() 138 | if !c.closed { 139 | c.log.Fatal(err) 140 | } 141 | c.wg.Done() 142 | }() 143 | 144 | return c, nil 145 | } 146 | 147 | func (c *Client) SetLogger(logger *log.Logger) { 148 | c.log = logger 149 | } 150 | 151 | // Close closes the client. 152 | func (c *Client) Close() error { 153 | c.log.Info("gateway: closing gateway client") 154 | c.closed = true 155 | close(c.udpSendChan) 156 | if err := c.conn.Close(); err != nil { 157 | return err 158 | } 159 | c.log.Info("gateway: handling last packets") 160 | c.wg.Wait() 161 | return nil 162 | } 163 | 164 | // RXPacketChan returns the channel containing the received RX packets. 165 | func (c *Client) RXPacketChan() chan RXPacketBytes { 166 | return c.rxChan 167 | } 168 | 169 | // StatsChan returns the channel containg the received gateway stats. 170 | func (c *Client) StatsChan() chan GatewayStatsPacket { 171 | return c.statsChan 172 | } 173 | 174 | // Send sends the given packet to the gateway. 175 | func (c *Client) Send(txPacket TXPacketBytes) error { 176 | gw, err := c.gateways.get(txPacket.TXInfo.MAC) 177 | if err != nil { 178 | return err 179 | } 180 | txpk, err := newTXPKFromTXPacket(txPacket) 181 | if err != nil { 182 | return err 183 | } 184 | pullResp := PullRespPacket{ 185 | ProtocolVersion: gw.protocolVersion, 186 | Payload: PullRespPayload{ 187 | TXPK: txpk, 188 | }, 189 | } 190 | bytes, err := pullResp.MarshalBinary() 191 | if err != nil { 192 | return fmt.Errorf("gateway: json marshall PullRespPacket error: %s", err) 193 | } 194 | c.udpSendChan <- udpPacket{ 195 | data: bytes, 196 | addr: gw.addr, 197 | } 198 | return nil 199 | } 200 | 201 | func (c *Client) readPackets() error { 202 | buf := make([]byte, 65507) // max udp data size 203 | for { 204 | i, addr, err := c.conn.ReadFromUDP(buf) 205 | if err != nil { 206 | return fmt.Errorf("gateway: read from udp error: %s", err) 207 | } 208 | data := make([]byte, i) 209 | copy(data, buf[:i]) 210 | go func(data []byte) { 211 | if err := c.handlePacket(addr, data); err != nil { 212 | c.log.WithFields(log.Fields{ 213 | "data_base64": base64.StdEncoding.EncodeToString(data), 214 | "addr": addr, 215 | }).Errorf("gateway: could not handle packet: %s", err) 216 | } 217 | }(data) 218 | } 219 | } 220 | 221 | func (c *Client) sendPackets() error { 222 | for p := range c.udpSendChan { 223 | pt, err := GetPacketType(p.data) 224 | if err != nil { 225 | c.log.WithFields(log.Fields{ 226 | "addr": p.addr, 227 | "data_base64": base64.StdEncoding.EncodeToString(p.data), 228 | }).Error("gateway: unknown packet type") 229 | continue 230 | } 231 | c.log.WithFields(log.Fields{ 232 | "addr": p.addr, 233 | "type": pt, 234 | "protocol_version": p.data[0], 235 | }).Info("gateway: sending udp packet to gateway") 236 | 237 | if _, err := c.conn.WriteToUDP(p.data, p.addr); err != nil { 238 | return err 239 | } 240 | } 241 | return nil 242 | } 243 | 244 | func (c *Client) handlePacket(addr *net.UDPAddr, data []byte) error { 245 | pt, err := GetPacketType(data) 246 | if err != nil { 247 | return err 248 | } 249 | c.log.WithFields(log.Fields{ 250 | "addr": addr, 251 | "type": pt, 252 | "protocol_version": data[0], 253 | }).Info("gateway: received udp packet from gateway") 254 | 255 | switch pt { 256 | case PushData: 257 | return c.handlePushData(addr, data) 258 | case PullData: 259 | return c.handlePullData(addr, data) 260 | case TXACK: 261 | return c.handleTXACK(addr, data) 262 | default: 263 | return fmt.Errorf("gateway: unknown packet type: %s", pt) 264 | } 265 | } 266 | 267 | func (b *Client) handlePullData(addr *net.UDPAddr, data []byte) error { 268 | var p PullDataPacket 269 | if err := p.UnmarshalBinary(data); err != nil { 270 | return err 271 | } 272 | ack := PullACKPacket{ 273 | ProtocolVersion: p.ProtocolVersion, 274 | RandomToken: p.RandomToken, 275 | } 276 | bytes, err := ack.MarshalBinary() 277 | if err != nil { 278 | return err 279 | } 280 | 281 | err = b.gateways.set(p.GatewayMAC, gateway{ 282 | addr: addr, 283 | lastSeen: time.Now().UTC(), 284 | protocolVersion: p.ProtocolVersion, 285 | }) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | b.udpSendChan <- udpPacket{ 291 | addr: addr, 292 | data: bytes, 293 | } 294 | return nil 295 | } 296 | 297 | func (b *Client) handlePushData(addr *net.UDPAddr, data []byte) error { 298 | var p PushDataPacket 299 | if err := p.UnmarshalBinary(data); err != nil { 300 | return err 301 | } 302 | 303 | // ack the packet 304 | ack := PushACKPacket{ 305 | ProtocolVersion: p.ProtocolVersion, 306 | RandomToken: p.RandomToken, 307 | } 308 | bytes, err := ack.MarshalBinary() 309 | if err != nil { 310 | return err 311 | } 312 | b.udpSendChan <- udpPacket{ 313 | addr: addr, 314 | data: bytes, 315 | } 316 | 317 | // gateway stats 318 | if p.Payload.Stat != nil { 319 | b.handleStat(addr, p.GatewayMAC, *p.Payload.Stat) 320 | } 321 | 322 | // rx packets 323 | for _, rxpk := range p.Payload.RXPK { 324 | if err := b.handleRXPacket(addr, p.GatewayMAC, rxpk); err != nil { 325 | return err 326 | } 327 | } 328 | return nil 329 | } 330 | 331 | func (c *Client) handleStat(addr *net.UDPAddr, mac Mac, stat Stat) { 332 | gwStats := newGatewayStatsPacket(mac, stat) 333 | c.log.WithFields(log.Fields{ 334 | "addr": addr, 335 | "mac": mac, 336 | }).Info("gateway: stat packet received") 337 | addIPToGatewayStatsPacket(&gwStats, addr.IP) 338 | if gtw, err := c.gateways.get(mac); err != nil && gtw.addr != nil { 339 | addIPToGatewayStatsPacket(&gwStats, gtw.addr.IP) 340 | } 341 | c.statsChan <- gwStats 342 | } 343 | 344 | func (c *Client) handleRXPacket(addr *net.UDPAddr, mac Mac, rxpk RXPK) error { 345 | logFields := log.Fields{ 346 | "addr": addr, 347 | "mac": mac, 348 | "data": rxpk.Data, 349 | } 350 | c.log.WithFields(logFields).Info("gateway: rxpk packet received") 351 | 352 | // decode packet 353 | rxPacket, err := newRXPacketFromRXPK(mac, rxpk) 354 | if err != nil { 355 | return err 356 | } 357 | 358 | // check CRC 359 | if c.CheckCrc && rxPacket.RXInfo.CRCStatus != 1 { 360 | c.log.WithFields(logFields).Warningf("gateway: invalid packet CRC: %d", rxPacket.RXInfo.CRCStatus) 361 | return errors.New("gateway: invalid CRC") 362 | } 363 | c.rxChan <- rxPacket 364 | return nil 365 | } 366 | 367 | func (c *Client) handleTXACK(addr *net.UDPAddr, data []byte) error { 368 | var p TXACKPacket 369 | if err := p.UnmarshalBinary(data); err != nil { 370 | return err 371 | } 372 | var errBool bool 373 | 374 | logFields := log.Fields{ 375 | "mac": p.GatewayMAC, 376 | "random_token": p.RandomToken, 377 | } 378 | if p.Payload != nil { 379 | if p.Payload.TXPKACK.Error != "NONE" { 380 | errBool = true 381 | } 382 | logFields["error"] = p.Payload.TXPKACK.Error 383 | } 384 | 385 | if errBool { 386 | c.log.WithFields(logFields).Error("gateway: tx ack received") 387 | } else { 388 | c.log.WithFields(logFields).Info("gateway: tx ack received") 389 | } 390 | 391 | return nil 392 | } 393 | 394 | // newGatewayStatsPacket from Stat transforms a Semtech Stat packet into a 395 | // GatewayStatsPacket. 396 | func newGatewayStatsPacket(mac Mac, stat Stat) GatewayStatsPacket { 397 | return GatewayStatsPacket{ 398 | Time: time.Time(stat.Time), 399 | MAC: mac, 400 | Latitude: stat.Lati, 401 | Longitude: stat.Long, 402 | Altitude: float64(stat.Alti), 403 | RXPacketsReceived: int(stat.RXNb), 404 | RXPacketsReceivedOK: int(stat.RXOK), 405 | CustomData: map[string]interface{}{ 406 | "platform": stat.Pfrm, 407 | "contactEmail": stat.Mail, 408 | "description": stat.Desc, 409 | "ip": []string{}, 410 | }, 411 | } 412 | } 413 | 414 | // newRXPacketFromRXPK transforms a Semtech packet into a RXPacketBytes. 415 | func newRXPacketFromRXPK(mac Mac, rxpk RXPK) (RXPacketBytes, error) { 416 | dataRate, err := newDataRateFromDatR(rxpk.DatR) 417 | if err != nil { 418 | return RXPacketBytes{}, fmt.Errorf("gateway: could not get DataRate from DatR: %s", err) 419 | } 420 | 421 | b, err := base64.StdEncoding.DecodeString(rxpk.Data) 422 | if err != nil { 423 | return RXPacketBytes{}, fmt.Errorf("gateway: could not base64 decode data: %s", err) 424 | } 425 | 426 | rxPacket := RXPacketBytes{ 427 | PHYPayload: b, 428 | RXInfo: RXInfo{ 429 | MAC: mac, 430 | Time: time.Time(rxpk.Time), 431 | Timestamp: rxpk.Tmst, 432 | Frequency: int(rxpk.Freq * 1000000), 433 | Channel: int(rxpk.Chan), 434 | RFChain: int(rxpk.RFCh), 435 | CRCStatus: int(rxpk.Stat), 436 | DataRate: dataRate, 437 | CodeRate: rxpk.CodR, 438 | RSSI: int(rxpk.RSSI), 439 | LoRaSNR: rxpk.LSNR, 440 | Size: int(rxpk.Size), 441 | }, 442 | } 443 | return rxPacket, nil 444 | } 445 | 446 | // newTXPKFromTXPacket transforms a TXPacketBytes into a Semtech 447 | // compatible packet. 448 | func newTXPKFromTXPacket(txPacket TXPacketBytes) (TXPK, error) { 449 | txpk := TXPK{ 450 | Imme: txPacket.TXInfo.Immediately, 451 | Tmst: txPacket.TXInfo.Timestamp, 452 | Freq: float64(txPacket.TXInfo.Frequency) / 1000000, 453 | Powe: uint8(txPacket.TXInfo.Power), 454 | Modu: string(txPacket.TXInfo.DataRate.Modulation), 455 | DatR: newDatRfromDataRate(txPacket.TXInfo.DataRate), 456 | CodR: txPacket.TXInfo.CodeRate, 457 | Size: uint16(len(txPacket.PHYPayload)), 458 | Data: base64.StdEncoding.EncodeToString(txPacket.PHYPayload), 459 | } 460 | 461 | if txPacket.TXInfo.DataRate.Modulation == band.FSKModulation { 462 | txpk.FDev = uint16(txPacket.TXInfo.DataRate.BitRate / 2) 463 | } 464 | 465 | // by default IPol=true is used for downlink LoRa modulation, however in 466 | // some cases one might want to override this. 467 | if txPacket.TXInfo.IPol != nil { 468 | txpk.IPol = *txPacket.TXInfo.IPol 469 | } else if txPacket.TXInfo.DataRate.Modulation == band.LoRaModulation { 470 | txpk.IPol = true 471 | } 472 | 473 | return txpk, nil 474 | } 475 | 476 | func newDataRateFromDatR(d DatR) (band.DataRate, error) { 477 | var dr band.DataRate 478 | 479 | if d.LoRa != "" { 480 | // parse e.g. SF12BW250 into separate variables 481 | match := loRaDataRateRegex.FindStringSubmatch(d.LoRa) 482 | if len(match) != 3 { 483 | return dr, errors.New("gateway: could not parse LoRa data rate") 484 | } 485 | 486 | // cast variables to ints 487 | sf, err := strconv.Atoi(match[1]) 488 | if err != nil { 489 | return dr, fmt.Errorf("gateway: could not convert spread factor to int: %s", err) 490 | } 491 | bw, err := strconv.Atoi(match[2]) 492 | if err != nil { 493 | return dr, fmt.Errorf("gateway: could not convert bandwith to int: %s", err) 494 | } 495 | 496 | dr.Modulation = band.LoRaModulation 497 | dr.SpreadFactor = sf 498 | dr.Bandwidth = bw 499 | return dr, nil 500 | } 501 | 502 | if d.FSK != 0 { 503 | dr.Modulation = band.FSKModulation 504 | dr.BitRate = int(d.FSK) 505 | return dr, nil 506 | } 507 | 508 | return dr, errors.New("gateway: could not convert DatR to DataRate, DatR is empty / modulation unknown") 509 | } 510 | 511 | func newDatRfromDataRate(d band.DataRate) DatR { 512 | if d.Modulation == band.LoRaModulation { 513 | return DatR{ 514 | LoRa: fmt.Sprintf("SF%dBW%d", d.SpreadFactor, d.Bandwidth), 515 | } 516 | } 517 | 518 | return DatR{ 519 | FSK: uint32(d.BitRate), 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /gateway/client_test.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "encoding/base64" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/Lobaro/lora-packet-forwarder-client/gateway/band" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func TestBackend(t *testing.T) { 14 | Convey("Given a new Backend binding at a random port", t, func() { 15 | backend, err := NewClient("127.0.0.1:0", nil, nil) 16 | So(err, ShouldBeNil) 17 | 18 | backendAddr, err := net.ResolveUDPAddr("udp", backend.conn.LocalAddr().String()) 19 | So(err, ShouldBeNil) 20 | 21 | Convey("Given a fake gateway UDP publisher", func() { 22 | gwAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") 23 | So(err, ShouldBeNil) 24 | gwConn, err := net.ListenUDP("udp", gwAddr) 25 | So(err, ShouldBeNil) 26 | defer gwConn.Close() 27 | So(gwConn.SetDeadline(time.Now().Add(time.Second)), ShouldBeNil) 28 | 29 | Convey("When sending a PULL_DATA packet", func() { 30 | p := PullDataPacket{ 31 | ProtocolVersion: ProtocolVersion2, 32 | RandomToken: 12345, 33 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 34 | } 35 | b, err := p.MarshalBinary() 36 | So(err, ShouldBeNil) 37 | _, err = gwConn.WriteToUDP(b, backendAddr) 38 | So(err, ShouldBeNil) 39 | 40 | Convey("Then an ACK packet is returned", func() { 41 | buf := make([]byte, 65507) 42 | i, _, err := gwConn.ReadFromUDP(buf) 43 | So(err, ShouldBeNil) 44 | var ack PullACKPacket 45 | So(ack.UnmarshalBinary(buf[:i]), ShouldBeNil) 46 | So(ack.RandomToken, ShouldEqual, p.RandomToken) 47 | So(ack.ProtocolVersion, ShouldEqual, p.ProtocolVersion) 48 | }) 49 | }) 50 | 51 | Convey("When sending a PUSH_DATA packet with stats", func() { 52 | p := PushDataPacket{ 53 | ProtocolVersion: ProtocolVersion2, 54 | RandomToken: 1234, 55 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 56 | Payload: PushDataPayload{ 57 | Stat: &Stat{ 58 | Time: ExpandedTime(time.Time{}.UTC()), 59 | Lati: 1.234, 60 | Long: 2.123, 61 | Alti: 123, 62 | RXNb: 1, 63 | RXOK: 2, 64 | RXFW: 3, 65 | ACKR: 33.3, 66 | DWNb: 4, 67 | Pfrm: "The Things Gateway", 68 | Mail: "admin@gateway.net", 69 | Desc: "My Gateway", 70 | }, 71 | }, 72 | } 73 | b, err := p.MarshalBinary() 74 | So(err, ShouldBeNil) 75 | _, err = gwConn.WriteToUDP(b, backendAddr) 76 | So(err, ShouldBeNil) 77 | 78 | Convey("Then an ACK packet is returned", func() { 79 | buf := make([]byte, 65507) 80 | i, _, err := gwConn.ReadFromUDP(buf) 81 | So(err, ShouldBeNil) 82 | var ack PushACKPacket 83 | So(ack.UnmarshalBinary(buf[:i]), ShouldBeNil) 84 | So(ack.RandomToken, ShouldEqual, p.RandomToken) 85 | So(ack.ProtocolVersion, ShouldEqual, p.ProtocolVersion) 86 | 87 | Convey("Then the gateway stats are returned by the stats channel", func() { 88 | stats := <-backend.StatsChan() 89 | So([8]byte(stats.MAC), ShouldEqual, [8]byte{1, 2, 3, 4, 5, 6, 7, 8}) 90 | }) 91 | }) 92 | }) 93 | 94 | Convey("When sending a PUSH_DATA packet with RXPK", func() { 95 | p := PushDataPacket{ 96 | ProtocolVersion: ProtocolVersion2, 97 | RandomToken: 1234, 98 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 99 | Payload: PushDataPayload{ 100 | RXPK: []RXPK{ 101 | { 102 | Time: CompactTime(time.Now().UTC()), 103 | Tmst: 708016819, 104 | Freq: 868.5, 105 | Chan: 2, 106 | RFCh: 1, 107 | Stat: 1, 108 | Modu: "LORA", 109 | DatR: DatR{LoRa: "SF7BW125"}, 110 | CodR: "4/5", 111 | RSSI: -51, 112 | LSNR: 7, 113 | Size: 16, 114 | Data: "QAEBAQGAAAABVfdjR6YrSw==", 115 | }, 116 | }, 117 | }, 118 | } 119 | b, err := p.MarshalBinary() 120 | So(err, ShouldBeNil) 121 | _, err = gwConn.WriteToUDP(b, backendAddr) 122 | So(err, ShouldBeNil) 123 | 124 | Convey("Then an ACK packet is returned", func() { 125 | buf := make([]byte, 65507) 126 | i, _, err := gwConn.ReadFromUDP(buf) 127 | So(err, ShouldBeNil) 128 | var ack PushACKPacket 129 | So(ack.UnmarshalBinary(buf[:i]), ShouldBeNil) 130 | So(ack.RandomToken, ShouldEqual, p.RandomToken) 131 | So(ack.ProtocolVersion, ShouldEqual, p.ProtocolVersion) 132 | }) 133 | 134 | Convey("Then the packet is returned by the RX packet channel", func() { 135 | rxPacket := <-backend.RXPacketChan() 136 | 137 | rxPacket2, err := newRXPacketFromRXPK(p.GatewayMAC, p.Payload.RXPK[0]) 138 | So(err, ShouldBeNil) 139 | So(rxPacket, ShouldResemble, rxPacket2) 140 | }) 141 | }) 142 | 143 | Convey("Given a TXPacket", func() { 144 | txPacket := TXPacketBytes{ 145 | TXInfo: TXInfo{ 146 | MAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 147 | Immediately: true, 148 | Timestamp: 12345, 149 | Frequency: 868100000, 150 | Power: 14, 151 | DataRate: band.DataRate{ 152 | Modulation: band.LoRaModulation, 153 | SpreadFactor: 12, 154 | Bandwidth: 250, 155 | }, 156 | CodeRate: "4/5", 157 | }, 158 | PHYPayload: []byte{1, 2, 3, 4}, 159 | } 160 | 161 | Convey("When sending the TXPacket and the gateway is not known to the backend", func() { 162 | err := backend.Send(txPacket) 163 | Convey("Then the backend returns an error", func() { 164 | So(err, ShouldEqual, errGatewayDoesNotExist) 165 | }) 166 | }) 167 | 168 | Convey("When sending the TXPacket when the gateway is known to the backend", func() { 169 | // sending a ping should register the gateway to the backend 170 | p := PullDataPacket{ 171 | ProtocolVersion: ProtocolVersion2, 172 | RandomToken: 12345, 173 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 174 | } 175 | b, err := p.MarshalBinary() 176 | So(err, ShouldBeNil) 177 | _, err = gwConn.WriteToUDP(b, backendAddr) 178 | So(err, ShouldBeNil) 179 | buf := make([]byte, 65507) 180 | i, _, err := gwConn.ReadFromUDP(buf) 181 | So(err, ShouldBeNil) 182 | var ack PullACKPacket 183 | So(ack.UnmarshalBinary(buf[:i]), ShouldBeNil) 184 | So(ack.RandomToken, ShouldEqual, p.RandomToken) 185 | So(ack.ProtocolVersion, ShouldEqual, p.ProtocolVersion) 186 | 187 | err = backend.Send(txPacket) 188 | 189 | Convey("Then no error is returned", func() { 190 | So(err, ShouldBeNil) 191 | }) 192 | 193 | Convey("Then the data is received by the gateway", func() { 194 | i, _, err := gwConn.ReadFromUDP(buf) 195 | So(err, ShouldBeNil) 196 | So(i, ShouldBeGreaterThan, 0) 197 | var pullResp PullRespPacket 198 | So(pullResp.UnmarshalBinary(buf[:i]), ShouldBeNil) 199 | 200 | So(pullResp, ShouldResemble, PullRespPacket{ 201 | ProtocolVersion: p.ProtocolVersion, 202 | Payload: PullRespPayload{ 203 | TXPK: TXPK{ 204 | Imme: true, 205 | Tmst: 12345, 206 | Freq: 868.1, 207 | Powe: 14, 208 | Modu: "LORA", 209 | DatR: DatR{ 210 | LoRa: "SF12BW250", 211 | }, 212 | CodR: "4/5", 213 | Size: uint16(len([]byte{1, 2, 3, 4})), 214 | Data: base64.StdEncoding.EncodeToString([]byte{1, 2, 3, 4}), 215 | IPol: true, 216 | }, 217 | }, 218 | }) 219 | }) 220 | }) 221 | }) 222 | }) 223 | }) 224 | } 225 | 226 | func TestNewGatewayStatPacket(t *testing.T) { 227 | Convey("Given a (Semtech) Stat struct and gateway MAC", t, func() { 228 | now := time.Now().UTC() 229 | stat := Stat{ 230 | Time: ExpandedTime(now), 231 | Lati: 1.234, 232 | Long: 2.123, 233 | Alti: 234, 234 | RXNb: 1, 235 | RXOK: 2, 236 | RXFW: 3, 237 | ACKR: 33.3, 238 | DWNb: 4, 239 | Pfrm: "The Things Gateway", 240 | Mail: "admin@gateway.net", 241 | Desc: "My Gateway", 242 | } 243 | mac := [8]byte{1, 2, 3, 4, 5, 6, 7, 8} 244 | 245 | Convey("When calling newGatewayStatsPacket", func() { 246 | gwStats := newGatewayStatsPacket(mac, stat) 247 | Convey("Then all fields are set correctly", func() { 248 | So(gwStats, ShouldResemble, GatewayStatsPacket{ 249 | Time: now, 250 | MAC: mac, 251 | Latitude: 1.234, 252 | Longitude: 2.123, 253 | Altitude: 234, 254 | RXPacketsReceived: 1, 255 | RXPacketsReceivedOK: 2, 256 | CustomData: map[string]interface{}{ 257 | "platform": "The Things Gateway", 258 | "contactEmail": "admin@gateway.net", 259 | "description": "My Gateway", 260 | "ip": []string{}, 261 | }, 262 | }) 263 | }) 264 | }) 265 | 266 | }) 267 | } 268 | 269 | func TestNewTXPKFromTXPacket(t *testing.T) { 270 | Convey("Given a TXPacket", t, func() { 271 | txPacket := TXPacketBytes{ 272 | TXInfo: TXInfo{ 273 | Timestamp: 12345, 274 | Frequency: 868100000, 275 | Power: 14, 276 | CodeRate: "4/5", 277 | DataRate: band.DataRate{ 278 | Modulation: band.LoRaModulation, 279 | SpreadFactor: 9, 280 | Bandwidth: 250, 281 | }, 282 | }, 283 | PHYPayload: []byte{1, 2, 3, 4}, 284 | } 285 | 286 | Convey("Then te expected TXPK is returned (with default IPol", func() { 287 | txpk, err := newTXPKFromTXPacket(txPacket) 288 | So(err, ShouldBeNil) 289 | So(txpk, ShouldResemble, TXPK{ 290 | Imme: false, 291 | Tmst: 12345, 292 | Freq: 868.1, 293 | Powe: 14, 294 | Modu: "LORA", 295 | DatR: DatR{ 296 | LoRa: "SF9BW250", 297 | }, 298 | CodR: "4/5", 299 | Size: 4, 300 | Data: "AQIDBA==", 301 | IPol: true, 302 | }) 303 | }) 304 | 305 | Convey("Given IPol is requested to false", func() { 306 | f := false 307 | txPacket.TXInfo.IPol = &f 308 | 309 | Convey("Then the TXPK IPol is set to false", func() { 310 | txpk, err := newTXPKFromTXPacket(txPacket) 311 | So(err, ShouldBeNil) 312 | So(txpk.IPol, ShouldBeFalse) 313 | }) 314 | }) 315 | }) 316 | } 317 | 318 | func TestNewRXPacketFromRXPK(t *testing.T) { 319 | Convey("Given a (Semtech) RXPK and gateway MAC", t, func() { 320 | now := time.Now().UTC() 321 | rxpk := RXPK{ 322 | Time: CompactTime(now), 323 | Tmst: 708016819, 324 | Freq: 868.5, 325 | Chan: 2, 326 | RFCh: 1, 327 | Stat: 1, 328 | Modu: "LORA", 329 | DatR: DatR{LoRa: "SF7BW125"}, 330 | CodR: "4/5", 331 | RSSI: -51, 332 | LSNR: 7, 333 | Size: 16, 334 | Data: base64.StdEncoding.EncodeToString([]byte{1, 2, 3, 4}), 335 | } 336 | mac := [8]byte{1, 2, 3, 4, 5, 6, 7, 8} 337 | 338 | Convey("When calling newRXPacketFromRXPK(", func() { 339 | rxPacket, err := newRXPacketFromRXPK(mac, rxpk) 340 | So(err, ShouldBeNil) 341 | 342 | Convey("Then all fields are set correctly", func() { 343 | So(rxPacket.PHYPayload, ShouldResemble, []byte{1, 2, 3, 4}) 344 | 345 | So(rxPacket.RXInfo, ShouldResemble, RXInfo{ 346 | MAC: mac, 347 | Time: now, 348 | Timestamp: 708016819, 349 | Frequency: 868500000, 350 | Channel: 2, 351 | RFChain: 1, 352 | CRCStatus: 1, 353 | DataRate: band.DataRate{ 354 | Modulation: band.LoRaModulation, 355 | SpreadFactor: 7, 356 | Bandwidth: 125, 357 | }, 358 | CodeRate: "4/5", 359 | RSSI: -51, 360 | LoRaSNR: 7, 361 | Size: 16, 362 | }) 363 | }) 364 | }) 365 | }) 366 | } 367 | 368 | func TestGatewaysCallbacks(t *testing.T) { 369 | Convey("Given a new gateways registry", t, func() { 370 | gw := gateways{ 371 | gateways: make(map[Mac]gateway), 372 | } 373 | 374 | mac := [8]byte{1, 2, 3, 4, 5, 6, 7, 8} 375 | 376 | Convey("Given a onNew and onDelete callback", func() { 377 | var onNewCalls int 378 | var onDeleteCalls int 379 | 380 | gw.onNew = func(mac Mac) error { 381 | onNewCalls = onNewCalls + 1 382 | return nil 383 | } 384 | 385 | gw.onDelete = func(mac Mac) error { 386 | onDeleteCalls = onDeleteCalls + 1 387 | return nil 388 | } 389 | 390 | Convey("When adding a new gateway", func() { 391 | So(gw.set(mac, gateway{}), ShouldBeNil) 392 | 393 | Convey("Then onNew callback is called once", func() { 394 | So(onNewCalls, ShouldEqual, 1) 395 | }) 396 | 397 | Convey("When updating the same gateway", func() { 398 | So(gw.set(mac, gateway{}), ShouldBeNil) 399 | 400 | Convey("Then onNew has not been called", func() { 401 | So(onNewCalls, ShouldEqual, 1) 402 | }) 403 | }) 404 | 405 | Convey("When cleaning up the gateways", func() { 406 | So(gw.cleanup(), ShouldBeNil) 407 | 408 | Convey("Then onDelete has been called once", func() { 409 | So(onDeleteCalls, ShouldEqual, 1) 410 | }) 411 | }) 412 | }) 413 | }) 414 | }) 415 | } 416 | -------------------------------------------------------------------------------- /gateway/ip.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func addIPToGatewayStatsPacket(stat *GatewayStatsPacket, ip net.IP) { 8 | var ips []string 9 | if existingIPs, ok := stat.CustomData["ip"]; ok { 10 | if existingIPs, ok := existingIPs.([]string); ok { 11 | for _, existingIP := range existingIPs { 12 | if ip.String() == existingIP { 13 | return 14 | } 15 | } 16 | ips = append(existingIPs, ip.String()) 17 | } 18 | } 19 | stat.CustomData["ip"] = ips 20 | } 21 | -------------------------------------------------------------------------------- /gateway/mac.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // 64 bit MacAddress of Gateway 10 | type Mac [8]byte 11 | 12 | // MarshalText implements encoding.TextMarshaler. 13 | func (m Mac) MarshalText() ([]byte, error) { 14 | return []byte(m.String()), nil 15 | } 16 | 17 | // UnmarshalText implements encoding.TextUnmarshaler. 18 | func (m *Mac) UnmarshalText(text []byte) error { 19 | b, err := hex.DecodeString(string(text)) 20 | if err != nil { 21 | return err 22 | } 23 | if len(m) != len(b) { 24 | return fmt.Errorf("lorawan: exactly %d bytes are expected", len(m)) 25 | } 26 | copy(m[:], b) 27 | return nil 28 | } 29 | 30 | // String implement fmt.Stringer. 31 | func (m Mac) String() string { 32 | return hex.EncodeToString(m[:]) 33 | } 34 | 35 | // MarshalBinary implements encoding.BinaryMarshaler. 36 | func (m Mac) MarshalBinary() ([]byte, error) { 37 | out := make([]byte, len(m)) 38 | // little endian 39 | for i, v := range m { 40 | out[len(m)-i-1] = v 41 | } 42 | return out, nil 43 | } 44 | 45 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 46 | func (m *Mac) UnmarshalBinary(data []byte) error { 47 | if len(data) != len(m) { 48 | return fmt.Errorf("lorawan: %d bytes of data are expected", len(m)) 49 | } 50 | for i, v := range data { 51 | // little endian 52 | m[len(m)-i-1] = v 53 | } 54 | return nil 55 | } 56 | 57 | // Scan implements sql.Scanner. 58 | func (m *Mac) Scan(src interface{}) error { 59 | b, ok := src.([]byte) 60 | if !ok { 61 | return errors.New("lorawan: []byte type expected") 62 | } 63 | if len(b) != len(m) { 64 | return fmt.Errorf("lorawan []byte must have length %d", len(m)) 65 | } 66 | copy(m[:], b) 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /gateway/mac_test.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestMac_MarshalText(t *testing.T) { 9 | m := Mac{1, 2, 3, 4, 5, 6, 7, 8} 10 | 11 | text, err := m.MarshalText() 12 | 13 | if err != nil { 14 | t.Error(err) 15 | } 16 | 17 | if !bytes.Equal(text, []byte("0102030405060708")) { 18 | t.Errorf("Expected text to be 12345678 but was %s", string(text)) 19 | } 20 | } 21 | 22 | func TestMac_UnmarshalText(t *testing.T) { 23 | m := Mac{} 24 | 25 | err := m.UnmarshalText([]byte("0102030405060708")) 26 | 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | if m.String() != "0102030405060708" { 32 | t.Errorf("Expected mac to be 0102030405060708 but was %s", m.String()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gateway/packets.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Lobaro/lora-packet-forwarder-client/gateway/band" 7 | ) 8 | 9 | // RXPacket contains the PHYPayload received from the gateway. 10 | type RXPacket struct { 11 | RXInfo RXInfo `json:"rxInfo"` 12 | PHYPayload []byte `json:"phyPayload"` 13 | } 14 | 15 | // RXPacketBytes contains the PHYPayload as []byte received from the gateway. 16 | // The JSON output is compatible with RXPacket. 17 | type RXPacketBytes struct { 18 | RXInfo RXInfo `json:"rxInfo"` 19 | PHYPayload []byte `json:"phyPayload"` 20 | } 21 | 22 | // RXInfo contains the RX information. 23 | type RXInfo struct { 24 | MAC Mac `json:"mac"` // MAC address of the gateway 25 | Time time.Time `json:"time,omitempty"` // receive time 26 | Timestamp uint32 `json:"timestamp"` // gateway internal receive timestamp with microsecond precision, will rollover every ~ 72 minutes 27 | Frequency int `json:"frequency"` // frequency in Hz 28 | Channel int `json:"channel"` // concentrator IF channel used for RX 29 | RFChain int `json:"rfChain"` // RF chain used for RX 30 | CRCStatus int `json:"crcStatus"` // 1 = OK, -1 = fail, 0 = no CRC 31 | CodeRate string `json:"codeRate"` // ECC code rate 32 | RSSI int `json:"rssi"` // RSSI in dBm 33 | LoRaSNR float64 `json:"loRaSNR"` // LoRa signal-to-noise ratio in dB 34 | Size int `json:"size"` // packet payload size 35 | DataRate band.DataRate `json:"dataRate"` // RX datarate (either LoRa or FSK) 36 | } 37 | 38 | // TXPacket contains the PHYPayload which should be send to the 39 | // gateway. 40 | type TXPacket struct { 41 | TXInfo TXInfo `json:"txInfo"` 42 | PHYPayload []byte `json:"phyPayload"` 43 | } 44 | 45 | // TXPacketBytes contains the PHYPayload as []byte which should be send to the 46 | // gateway. The JSON output is compatible with TXPacket. 47 | type TXPacketBytes struct { 48 | TXInfo TXInfo `json:"txInfo"` 49 | PHYPayload []byte `json:"phyPayload"` 50 | } 51 | 52 | // TXInfo contains the information used for TX. 53 | type TXInfo struct { 54 | MAC Mac `json:"mac"` // MAC address of the gateway 55 | Immediately bool `json:"immediately"` // send the packet immediately (ignore Time) 56 | Timestamp uint32 `json:"timestamp"` // gateway internal receive timestamp with microsecond precision, will rollover every ~ 72 minutes 57 | Frequency int `json:"frequency"` // frequency in Hz 58 | Power int `json:"power"` // TX power to use in dBm 59 | DataRate band.DataRate `json:"dataRate"` // TX datarate (either LoRa or FSK) 60 | CodeRate string `json:"codeRate"` // ECC code rate 61 | IPol *bool `json:"iPol"` // when left nil, the gateway-bridge will use the default (true for LoRa modulation) 62 | } 63 | 64 | // GatewayStatsPacket contains the information of a gateway. 65 | type GatewayStatsPacket struct { 66 | MAC Mac `json:"mac"` 67 | Time time.Time `json:"time,omitempty"` 68 | Latitude float64 `json:"latitude"` 69 | Longitude float64 `json:"longitude"` 70 | Altitude float64 `json:"altitude"` 71 | RXPacketsReceived int `json:"rxPacketsReceived"` 72 | RXPacketsReceivedOK int `json:"rxPacketsReceivedOK"` 73 | TXPacketsEmitted int `json:"txPacketsEmitted"` 74 | CustomData map[string]interface{} `json:"customData"` // custom fields defined by alternative packet_forwarder versions (e.g. TTN sends platform, contactEmail, and description) 75 | } 76 | -------------------------------------------------------------------------------- /gateway/packettype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=PacketType"; DO NOT EDIT 2 | 3 | package gateway 4 | 5 | import "fmt" 6 | 7 | const _PacketType_name = "PushDataPushACKPullDataPullRespPullACKTXACK" 8 | 9 | var _PacketType_index = [...]uint8{0, 8, 15, 23, 31, 38, 43} 10 | 11 | func (i PacketType) String() string { 12 | if i >= PacketType(len(_PacketType_index)-1) { 13 | return fmt.Sprintf("PacketType(%d)", i) 14 | } 15 | return _PacketType_name[_PacketType_index[i]:_PacketType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /gateway/structs.go: -------------------------------------------------------------------------------- 1 | //go:generate stringer -type=PacketType 2 | 3 | package gateway 4 | 5 | import ( 6 | "encoding/binary" 7 | "encoding/json" 8 | "errors" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // PacketType defines the packet type. 15 | type PacketType byte 16 | 17 | // Available packet types 18 | const ( 19 | PushData PacketType = iota 20 | PushACK 21 | PullData 22 | PullResp 23 | PullACK 24 | TXACK 25 | ) 26 | 27 | // Protocol versions 28 | const ( 29 | ProtocolVersion1 uint8 = 0x01 30 | ProtocolVersion2 uint8 = 0x02 31 | ) 32 | 33 | // Errors 34 | var ( 35 | ErrInvalidProtocolVersion = errors.New("gateway: invalid protocol version") 36 | ) 37 | 38 | func protocolSupported(p uint8) bool { 39 | if p == ProtocolVersion1 || p == ProtocolVersion2 { 40 | return true 41 | } 42 | return false 43 | } 44 | 45 | // PushDataPacket type is used by the gateway mainly to forward the RF packets 46 | // received, and associated metadata, to the server. 47 | type PushDataPacket struct { 48 | ProtocolVersion uint8 49 | RandomToken uint16 50 | GatewayMAC Mac 51 | Payload PushDataPayload 52 | } 53 | 54 | // MarshalBinary marshals the object in binary form. 55 | func (p PushDataPacket) MarshalBinary() ([]byte, error) { 56 | pb, err := json.Marshal(&p.Payload) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | out := make([]byte, 4, len(pb)+12) 62 | out[0] = p.ProtocolVersion 63 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 64 | out[3] = byte(PushData) 65 | out = append(out, p.GatewayMAC[0:len(p.GatewayMAC)]...) 66 | out = append(out, pb...) 67 | return out, nil 68 | } 69 | 70 | // UnmarshalBinary decodes the object from binary form. 71 | func (p *PushDataPacket) UnmarshalBinary(data []byte) error { 72 | if len(data) < 13 { 73 | return errors.New("gateway: at least 13 bytes are expected") 74 | } 75 | if data[3] != byte(PushData) { 76 | return errors.New("gateway: identifier mismatch (PUSH_DATA expected)") 77 | } 78 | 79 | if !protocolSupported(data[0]) { 80 | return ErrInvalidProtocolVersion 81 | } 82 | 83 | p.ProtocolVersion = data[0] 84 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 85 | for i := 0; i < 8; i++ { 86 | p.GatewayMAC[i] = data[4+i] 87 | } 88 | 89 | return json.Unmarshal(data[12:], &p.Payload) 90 | } 91 | 92 | // PushACKPacket is used by the server to acknowledge immediately all the 93 | // PUSH_DATA packets received. 94 | type PushACKPacket struct { 95 | ProtocolVersion uint8 96 | RandomToken uint16 97 | } 98 | 99 | // MarshalBinary marshals the object in binary form. 100 | func (p PushACKPacket) MarshalBinary() ([]byte, error) { 101 | out := make([]byte, 4) 102 | out[0] = p.ProtocolVersion 103 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 104 | out[3] = byte(PushACK) 105 | return out, nil 106 | } 107 | 108 | // UnmarshalBinary decodes the object from binary form. 109 | func (p *PushACKPacket) UnmarshalBinary(data []byte) error { 110 | if len(data) != 4 { 111 | return errors.New("gateway: 4 bytes of data are expected") 112 | } 113 | if data[3] != byte(PushACK) { 114 | return errors.New("gateway: identifier mismatch (PUSH_ACK expected)") 115 | } 116 | 117 | if !protocolSupported(data[0]) { 118 | return ErrInvalidProtocolVersion 119 | } 120 | p.ProtocolVersion = data[0] 121 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 122 | return nil 123 | } 124 | 125 | // PullDataPacket is used by the gateway to poll data from the server. 126 | type PullDataPacket struct { 127 | ProtocolVersion uint8 128 | RandomToken uint16 129 | GatewayMAC [8]byte 130 | } 131 | 132 | // MarshalBinary marshals the object in binary form. 133 | func (p PullDataPacket) MarshalBinary() ([]byte, error) { 134 | out := make([]byte, 4, 12) 135 | out[0] = p.ProtocolVersion 136 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 137 | out[3] = byte(PullData) 138 | out = append(out, p.GatewayMAC[0:len(p.GatewayMAC)]...) 139 | return out, nil 140 | } 141 | 142 | // UnmarshalBinary decodes the object from binary form. 143 | func (p *PullDataPacket) UnmarshalBinary(data []byte) error { 144 | if len(data) != 12 { 145 | return errors.New("gateway: 12 bytes of data are expected") 146 | } 147 | if data[3] != byte(PullData) { 148 | return errors.New("gateway: identifier mismatch (PULL_DATA expected)") 149 | } 150 | 151 | if !protocolSupported(data[0]) { 152 | return ErrInvalidProtocolVersion 153 | } 154 | p.ProtocolVersion = data[0] 155 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 156 | for i := 0; i < 8; i++ { 157 | p.GatewayMAC[i] = data[4+i] 158 | } 159 | return nil 160 | } 161 | 162 | // PullACKPacket is used by the server to confirm that the network route is 163 | // open and that the server can send PULL_RESP packets at any time. 164 | type PullACKPacket struct { 165 | ProtocolVersion uint8 166 | RandomToken uint16 167 | } 168 | 169 | // MarshalBinary marshals the object in binary form. 170 | func (p PullACKPacket) MarshalBinary() ([]byte, error) { 171 | out := make([]byte, 4) 172 | out[0] = p.ProtocolVersion 173 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 174 | out[3] = byte(PullACK) 175 | return out, nil 176 | } 177 | 178 | // UnmarshalBinary decodes the object from binary form. 179 | func (p *PullACKPacket) UnmarshalBinary(data []byte) error { 180 | if len(data) != 4 { 181 | return errors.New("gateway: 4 bytes of data are expected") 182 | } 183 | if data[3] != byte(PullACK) { 184 | return errors.New("gateway: identifier mismatch (PULL_ACK expected)") 185 | } 186 | if !protocolSupported(data[0]) { 187 | return ErrInvalidProtocolVersion 188 | } 189 | p.ProtocolVersion = data[0] 190 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 191 | return nil 192 | } 193 | 194 | // PullRespPacket is used by the server to send RF packets and associated 195 | // metadata that will have to be emitted by the gateway. 196 | type PullRespPacket struct { 197 | ProtocolVersion uint8 198 | RandomToken uint16 199 | Payload PullRespPayload 200 | } 201 | 202 | // MarshalBinary marshals the object in binary form. 203 | func (p PullRespPacket) MarshalBinary() ([]byte, error) { 204 | pb, err := json.Marshal(&p.Payload) 205 | if err != nil { 206 | return nil, err 207 | } 208 | out := make([]byte, 4, 4+len(pb)) 209 | out[0] = p.ProtocolVersion 210 | 211 | if p.ProtocolVersion != ProtocolVersion1 { 212 | // these two bytes are unused in ProtocolVersion1 213 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 214 | } 215 | out[3] = byte(PullResp) 216 | out = append(out, pb...) 217 | return out, nil 218 | } 219 | 220 | // UnmarshalBinary decodes the object from binary form. 221 | func (p *PullRespPacket) UnmarshalBinary(data []byte) error { 222 | if len(data) < 5 { 223 | return errors.New("gateway: at least 5 bytes of data are expected") 224 | } 225 | if data[3] != byte(PullResp) { 226 | return errors.New("gateway: identifier mismatch (PULL_RESP expected)") 227 | } 228 | if !protocolSupported(data[0]) { 229 | return ErrInvalidProtocolVersion 230 | } 231 | p.ProtocolVersion = data[0] 232 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 233 | return json.Unmarshal(data[4:], &p.Payload) 234 | } 235 | 236 | // TXACKPacket is used by the gateway to send a feedback to the server 237 | // to inform if a downlink request has been accepted or rejected by the 238 | // gateway. 239 | type TXACKPacket struct { 240 | ProtocolVersion uint8 241 | RandomToken uint16 242 | GatewayMAC Mac 243 | Payload *TXACKPayload 244 | } 245 | 246 | // MarshalBinary marshals the object into binary form. 247 | func (p TXACKPacket) MarshalBinary() ([]byte, error) { 248 | var pb []byte 249 | if p.Payload != nil { 250 | var err error 251 | pb, err = json.Marshal(p.Payload) 252 | if err != nil { 253 | return nil, err 254 | } 255 | } 256 | 257 | out := make([]byte, 4, len(pb)+12) 258 | out[0] = p.ProtocolVersion 259 | binary.LittleEndian.PutUint16(out[1:3], p.RandomToken) 260 | out[3] = byte(TXACK) 261 | out = append(out, p.GatewayMAC[:]...) 262 | out = append(out, pb...) 263 | return out, nil 264 | } 265 | 266 | // UnmarshalBinary decodes the object from binary form. 267 | func (p *TXACKPacket) UnmarshalBinary(data []byte) error { 268 | if len(data) < 12 { 269 | return errors.New("gateway: at least 12 bytes of data are expected") 270 | } 271 | if data[3] != byte(TXACK) { 272 | return errors.New("gateway: identifier mismatch (TXACK expected)") 273 | } 274 | if !protocolSupported(data[0]) { 275 | return ErrInvalidProtocolVersion 276 | } 277 | p.ProtocolVersion = data[0] 278 | p.RandomToken = binary.LittleEndian.Uint16(data[1:3]) 279 | for i := 0; i < 8; i++ { 280 | p.GatewayMAC[i] = data[4+i] 281 | } 282 | if len(data) > 12 { 283 | p.Payload = &TXACKPayload{} 284 | return json.Unmarshal(data[12:], p.Payload) 285 | } 286 | return nil 287 | } 288 | 289 | // TXACKPayload contains the TXACKPacket payload. 290 | type TXACKPayload struct { 291 | TXPKACK TXPKACK `json:"txpk_ack"` 292 | } 293 | 294 | // TXPKACK contains the status information of the associated PULL_RESP 295 | // packet. 296 | type TXPKACK struct { 297 | Error string `json:"error"` 298 | } 299 | 300 | // PushDataPayload represents the upstream JSON data structure. 301 | type PushDataPayload struct { 302 | RXPK []RXPK `json:"rxpk,omitempty"` 303 | Stat *Stat `json:"stat,omitempty"` 304 | } 305 | 306 | // PullRespPayload represents the downstream JSON data structure. 307 | type PullRespPayload struct { 308 | TXPK TXPK `json:"txpk"` 309 | } 310 | 311 | // CompactTime implements time.Time but (un)marshals to and from 312 | // ISO 8601 'compact' format. 313 | type CompactTime time.Time 314 | 315 | // MarshalJSON implements the json.Marshaler interface. 316 | func (t CompactTime) MarshalJSON() ([]byte, error) { 317 | return []byte(time.Time(t).UTC().Format(`"` + time.RFC3339Nano + `"`)), nil 318 | } 319 | 320 | // UnmarshalJSON implements the json.Unmarshaler interface. 321 | func (t *CompactTime) UnmarshalJSON(data []byte) error { 322 | t2, err := time.Parse(`"`+time.RFC3339Nano+`"`, string(data)) 323 | if err != nil { 324 | return err 325 | } 326 | *t = CompactTime(t2) 327 | return nil 328 | } 329 | 330 | // ExpandedTime implements time.Time but (un)marshals to and from 331 | // ISO 8601 'expanded' format. 332 | type ExpandedTime time.Time 333 | 334 | // MarshalJSON implements the json.Marshaler interface. 335 | func (t ExpandedTime) MarshalJSON() ([]byte, error) { 336 | return []byte(time.Time(t).UTC().Format(`"2006-01-02 15:04:05 MST"`)), nil 337 | } 338 | 339 | // UnmarshalJSON implements the json.Unmarshaler interface. 340 | func (t *ExpandedTime) UnmarshalJSON(data []byte) error { 341 | t2, err := time.Parse(`"2006-01-02 15:04:05 MST"`, string(data)) 342 | if err != nil { 343 | return err 344 | } 345 | *t = ExpandedTime(t2) 346 | return nil 347 | } 348 | 349 | // DatR implements the data rate which can be either a string (LoRa identifier) 350 | // or an unsigned integer in case of FSK (bits per second). 351 | type DatR struct { 352 | LoRa string 353 | FSK uint32 354 | } 355 | 356 | // MarshalJSON implements the json.Marshaler interface. 357 | func (d DatR) MarshalJSON() ([]byte, error) { 358 | if d.LoRa != "" { 359 | return []byte(`"` + d.LoRa + `"`), nil 360 | } 361 | return []byte(strconv.FormatUint(uint64(d.FSK), 10)), nil 362 | } 363 | 364 | // UnmarshalJSON implements the json.Unmarshaler interface. 365 | func (d *DatR) UnmarshalJSON(data []byte) error { 366 | i, err := strconv.ParseUint(string(data), 10, 32) 367 | if err != nil { 368 | d.LoRa = strings.Trim(string(data), `"`) 369 | return nil 370 | } 371 | d.FSK = uint32(i) 372 | return nil 373 | } 374 | 375 | // RXPK contain a RF packet and associated metadata. 376 | type RXPK struct { 377 | Time CompactTime `json:"time"` // UTC time of pkt RX, us precision, ISO 8601 'compact' format (e.g. 2013-03-31T16:21:17.528002Z) 378 | Tmst uint32 `json:"tmst"` // Internal timestamp of "RX finished" event (32b unsigned) 379 | Freq float64 `json:"freq"` // RX central frequency in MHz (unsigned float, Hz precision) 380 | Chan uint8 `json:"chan"` // Concentrator "IF" channel used for RX (unsigned integer) 381 | RFCh uint8 `json:"rfch"` // Concentrator "RF chain" used for RX (unsigned integer) 382 | Stat int8 `json:"stat"` // CRC status: 1 = OK, -1 = fail, 0 = no CRC 383 | Modu string `json:"modu"` // Modulation identifier "LORA" or "FSK" 384 | DatR DatR `json:"datr"` // LoRa datarate identifier (eg. SF12BW500) || FSK datarate (unsigned, in bits per second) 385 | CodR string `json:"codr"` // LoRa ECC coding rate identifier 386 | RSSI int16 `json:"rssi"` // RSSI in dBm (signed integer, 1 dB precision) 387 | LSNR float64 `json:"lsnr"` // Lora SNR ratio in dB (signed float, 0.1 dB precision) 388 | Size uint16 `json:"size"` // RF packet payload size in bytes (unsigned integer) 389 | Data string `json:"data"` // Base64 encoded RF packet payload, padded 390 | } 391 | 392 | // Stat contains the status of the gateway. 393 | type Stat struct { 394 | Time ExpandedTime `json:"time"` // UTC 'system' time of the gateway, ISO 8601 'expanded' format (e.g 2014-01-12 08:59:28 GMT) 395 | Lati float64 `json:"lati"` // GPS latitude of the gateway in degree (float, N is +) 396 | Long float64 `json:"long"` // GPS latitude of the gateway in degree (float, E is +) 397 | Alti int32 `json:"alti"` // GPS altitude of the gateway in meter RX (integer) 398 | RXNb uint32 `json:"rxnb"` // Number of radio packets received (unsigned integer) 399 | RXOK uint32 `json:"rxok"` // Number of radio packets received with a valid PHY CRC 400 | RXFW uint32 `json:"rxfw"` // Number of radio packets forwarded (unsigned integer) 401 | ACKR float64 `json:"ackr"` // Percentage of upstream datagrams that were acknowledged 402 | DWNb uint32 `json:"dwnb"` // Number of downlink datagrams received (unsigned integer) 403 | Pfrm string `json:"pfrm"` // Platform definition, max 24 chars 404 | Mail string `json:"mail"` // Email of gateway operator, max 40 chars 405 | Desc string `json:"desc"` // Public description of this device, max 64 chars 406 | } 407 | 408 | // TXPK contains a RF packet to be emitted and associated metadata. 409 | type TXPK struct { 410 | Imme bool `json:"imme"` // Send packet immediately (will ignore tmst & time) 411 | Tmst uint32 `json:"tmst,omitempty"` // Send packet on a certain timestamp value (will ignore time) 412 | Time *CompactTime `json:"time,omitempty"` // Send packet at a certain time (GPS synchronization required) 413 | Freq float64 `json:"freq"` // TX central frequency in MHz (unsigned float, Hz precision) 414 | RFCh uint8 `json:"rfch"` // Concentrator "RF chain" used for TX (unsigned integer) 415 | Powe uint8 `json:"powe"` // TX output power in dBm (unsigned integer, dBm precision) 416 | Modu string `json:"modu"` // Modulation identifier "LORA" or "FSK" 417 | DatR DatR `json:"datr"` // LoRa datarate identifier (eg. SF12BW500) || FSK datarate (unsigned, in bits per second) 418 | CodR string `json:"codr,omitempty"` // LoRa ECC coding rate identifier 419 | FDev uint16 `json:"fdev,omitempty"` // FSK frequency deviation (unsigned integer, in Hz) 420 | IPol bool `json:"ipol"` // Lora modulation polarization inversion 421 | Prea uint16 `json:"prea,omitempty"` // RF preamble size (unsigned integer) 422 | Size uint16 `json:"size"` // RF packet payload size in bytes (unsigned integer) 423 | NCRC bool `json:"ncrc,omitempty"` // If true, disable the CRC of the physical layer (optional) 424 | Data string `json:"data"` // Base64 encoded RF packet payload, padding optional 425 | } 426 | 427 | // GetPacketType returns the packet type for the given packet data. 428 | func GetPacketType(data []byte) (PacketType, error) { 429 | if len(data) < 4 { 430 | return PacketType(0), errors.New("gateway: at least 4 bytes of data are expected") 431 | } 432 | 433 | if !protocolSupported(data[0]) { 434 | return PacketType(0), ErrInvalidProtocolVersion 435 | } 436 | 437 | return PacketType(data[3]), nil 438 | } 439 | -------------------------------------------------------------------------------- /gateway/structs_test.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestDatR(t *testing.T) { 12 | Convey("Given an empty DatR", t, func() { 13 | var d DatR 14 | 15 | Convey("Then MarshalJSON returns '0'", func() { 16 | b, err := d.MarshalJSON() 17 | So(err, ShouldBeNil) 18 | So(string(b), ShouldEqual, "0") 19 | }) 20 | 21 | Convey("Given LoRa=SF7BW125", func() { 22 | d.LoRa = "SF7BW125" 23 | Convey("Then MarshalJSON returns '\"SF7BW125\"'", func() { 24 | b, err := d.MarshalJSON() 25 | So(err, ShouldBeNil) 26 | So(string(b), ShouldEqual, `"SF7BW125"`) 27 | }) 28 | }) 29 | 30 | Convey("Given FSK=1234", func() { 31 | d.FSK = 1234 32 | Convey("Then MarshalJSON returns '1234'", func() { 33 | b, err := d.MarshalJSON() 34 | So(err, ShouldBeNil) 35 | So(string(b), ShouldEqual, "1234") 36 | }) 37 | }) 38 | 39 | Convey("Given the string '1234'", func() { 40 | s := "1234" 41 | Convey("Then UnmarshalJSON returns FSK=1234", func() { 42 | err := d.UnmarshalJSON([]byte(s)) 43 | So(err, ShouldBeNil) 44 | So(d.FSK, ShouldEqual, 1234) 45 | }) 46 | }) 47 | 48 | Convey("Given the string '\"SF7BW125\"'", func() { 49 | s := `"SF7BW125"` 50 | Convey("Then UnmarshalJSON returns LoRa=SF7BW125", func() { 51 | err := d.UnmarshalJSON([]byte(s)) 52 | So(err, ShouldBeNil) 53 | So(d.LoRa, ShouldEqual, "SF7BW125") 54 | }) 55 | }) 56 | }) 57 | } 58 | 59 | func TestCompactTime(t *testing.T) { 60 | Convey("Given the date 'Mon Jan 2 15:04:05 -0700 MST 2006'", t, func() { 61 | tStr := "Mon Jan 2 15:04:05 -0700 MST 2006" 62 | ts, err := time.Parse(tStr, tStr) 63 | So(err, ShouldBeNil) 64 | 65 | Convey("MarshalJSON returns '\"2006-01-02T22:04:05Z\"'", func() { 66 | 67 | b, err := CompactTime(ts).MarshalJSON() 68 | So(err, ShouldBeNil) 69 | So(string(b), ShouldEqual, `"2006-01-02T22:04:05Z"`) 70 | }) 71 | 72 | Convey("Given the JSON value of the date (\"2006-01-02T22:04:05Z\")", func() { 73 | s := `"2006-01-02T22:04:05Z"` 74 | Convey("UnmarshalJSON returns the correct date", func() { 75 | var ct CompactTime 76 | err := ct.UnmarshalJSON([]byte(s)) 77 | So(err, ShouldBeNil) 78 | So(time.Time(ct).Equal(ts), ShouldBeTrue) 79 | }) 80 | }) 81 | }) 82 | } 83 | 84 | func TestGetPacketType(t *testing.T) { 85 | Convey("Given an empty slice []byte{}", t, func() { 86 | var b []byte 87 | 88 | Convey("Then GetPacketType returns an error (length)", func() { 89 | _, err := GetPacketType(b) 90 | So(err, ShouldResemble, errors.New("gateway: at least 4 bytes of data are expected")) 91 | }) 92 | 93 | Convey("Given the slice []byte{3, 1, 3, 4}", func() { 94 | b = []byte{3, 1, 3, 4} 95 | Convey("Then GetPacketType returns an error (protocol version)", func() { 96 | _, err := GetPacketType(b) 97 | So(err, ShouldResemble, ErrInvalidProtocolVersion) 98 | }) 99 | }) 100 | 101 | Convey("Given the slice []byte{2, 1, 3, 4}", func() { 102 | b = []byte{2, 1, 3, 4} 103 | Convey("Then GetPacketType returns PullACK", func() { 104 | t, err := GetPacketType(b) 105 | So(err, ShouldBeNil) 106 | So(t, ShouldEqual, PullACK) 107 | }) 108 | }) 109 | }) 110 | } 111 | 112 | func TestPushDataPacket(t *testing.T) { 113 | Convey("Given an empty PushDataPacket", t, func() { 114 | var p PushDataPacket 115 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 125}", func() { 116 | b, err := p.MarshalBinary() 117 | So(err, ShouldBeNil) 118 | So(b, ShouldResemble, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 125}) 119 | }) 120 | 121 | Convey("Given ProtocolVersion=2, RandomToken=123, GatewayMAC=[]{2, 2, 3, 4, 5, 6, 7, 8}", func() { 122 | p = PushDataPacket{ 123 | ProtocolVersion: ProtocolVersion2, 124 | RandomToken: 123, 125 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 126 | } 127 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125}", func() { 128 | b, err := p.MarshalBinary() 129 | So(err, ShouldBeNil) 130 | So(b, ShouldResemble, []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125}) 131 | }) 132 | }) 133 | 134 | Convey("Given the slice []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125}", func() { 135 | b := []byte{2, 123, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 123, 125} 136 | Convey("Then UnmarshalBinary returns RandomToken=123, GatewayMAC=[]{1, 2, 3, 4, 5, 6, 7, 8}", func() { 137 | err := p.UnmarshalBinary(b) 138 | So(err, ShouldBeNil) 139 | So(p, ShouldResemble, PushDataPacket{ 140 | ProtocolVersion: ProtocolVersion2, 141 | RandomToken: 123, 142 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 143 | }) 144 | }) 145 | }) 146 | }) 147 | } 148 | 149 | func TestPushACKPacket(t *testing.T) { 150 | Convey("Given an empty PushACKPacket", t, func() { 151 | var p PushACKPacket 152 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 1}", func() { 153 | b, err := p.MarshalBinary() 154 | So(err, ShouldBeNil) 155 | So(b, ShouldResemble, []byte{0, 0, 0, 1}) 156 | }) 157 | 158 | Convey("Given ProtocolVersion=2, RandomToken=123", func() { 159 | p = PushACKPacket{ 160 | ProtocolVersion: ProtocolVersion2, 161 | RandomToken: 123, 162 | } 163 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 1}", func() { 164 | b, err := p.MarshalBinary() 165 | So(err, ShouldBeNil) 166 | So(b, ShouldResemble, []byte{2, 123, 0, 1}) 167 | }) 168 | }) 169 | 170 | Convey("Given the slice []byte{2, 123, 0, 1}", func() { 171 | Convey("Then UnmarshalBinary returns RandomToken=123", func() { 172 | b := []byte{2, 123, 0, 1} 173 | err := p.UnmarshalBinary(b) 174 | So(err, ShouldBeNil) 175 | So(p, ShouldResemble, PushACKPacket{ 176 | ProtocolVersion: ProtocolVersion2, 177 | RandomToken: 123, 178 | }) 179 | }) 180 | }) 181 | }) 182 | } 183 | 184 | func TestPullDataPacket(t *testing.T) { 185 | Convey("Given an empty PullDataPacket", t, func() { 186 | var p PullDataPacket 187 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}", func() { 188 | b, err := p.MarshalBinary() 189 | So(err, ShouldBeNil) 190 | So(b, ShouldResemble, []byte{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}) 191 | }) 192 | 193 | Convey("Given ProtocolVersion=2, RandomToken=123, GatewayMAC=[]byte{1, 2, 3, 4, 5, 6, 8, 8}", func() { 194 | p = PullDataPacket{ 195 | ProtocolVersion: ProtocolVersion2, 196 | RandomToken: 123, 197 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 198 | } 199 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}", func() { 200 | b, err := p.MarshalBinary() 201 | So(err, ShouldBeNil) 202 | So(b, ShouldResemble, []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}) 203 | }) 204 | }) 205 | 206 | Convey("Given the slice []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8}", func() { 207 | b := []byte{2, 123, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8} 208 | Convey("Then UnmarshalBinary returns RandomToken=123, GatewayMAC=[]byte{1, 2, 3, 4, 5, 6, 8, 8}", func() { 209 | err := p.UnmarshalBinary(b) 210 | So(err, ShouldBeNil) 211 | So(p, ShouldResemble, PullDataPacket{ 212 | ProtocolVersion: ProtocolVersion2, 213 | RandomToken: 123, 214 | GatewayMAC: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 215 | }) 216 | }) 217 | }) 218 | }) 219 | } 220 | 221 | func TestPullACKPacket(t *testing.T) { 222 | Convey("Given an empty PullACKPacket", t, func() { 223 | var p PullACKPacket 224 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 4}", func() { 225 | b, err := p.MarshalBinary() 226 | So(err, ShouldBeNil) 227 | So(b, ShouldResemble, []byte{0, 0, 0, 4}) 228 | }) 229 | 230 | Convey("Given ProtocolVersion=2, RandomToken=123}", func() { 231 | p = PullACKPacket{ 232 | ProtocolVersion: ProtocolVersion2, 233 | RandomToken: 123, 234 | } 235 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 4}", func() { 236 | b, err := p.MarshalBinary() 237 | So(err, ShouldBeNil) 238 | So(b, ShouldResemble, []byte{2, 123, 0, 4}) 239 | }) 240 | }) 241 | 242 | Convey("Given the slice []byte{2, 123, 0, 4}", func() { 243 | b := []byte{2, 123, 0, 4} 244 | Convey("Then UnmarshalBinary returns RandomToken=123", func() { 245 | err := p.UnmarshalBinary(b) 246 | So(err, ShouldBeNil) 247 | So(p, ShouldResemble, PullACKPacket{ 248 | ProtocolVersion: ProtocolVersion2, 249 | RandomToken: 123, 250 | }) 251 | }) 252 | }) 253 | }) 254 | } 255 | 256 | func TestPullRespPacket(t *testing.T) { 257 | Convey("Given an empty PullRespPacket", t, func() { 258 | var p PullRespPacket 259 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 3} as first 4 bytes", func() { 260 | b, err := p.MarshalBinary() 261 | So(err, ShouldBeNil) 262 | So(b[0:4], ShouldResemble, []byte{0, 0, 0, 3}) 263 | }) 264 | 265 | Convey("Given ProtocolVersion=2, RandomToken=123", func() { 266 | p = PullRespPacket{ 267 | ProtocolVersion: ProtocolVersion2, 268 | RandomToken: 123, 269 | } 270 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 3} as first 4 bytes", func() { 271 | b, err := p.MarshalBinary() 272 | So(err, ShouldBeNil) 273 | So(b[0:4], ShouldResemble, []byte{2, 123, 0, 3}) 274 | }) 275 | }) 276 | 277 | Convey("Given ProtocolVersion=1, RandomToken=123", func() { 278 | p = PullRespPacket{ 279 | ProtocolVersion: ProtocolVersion1, 280 | RandomToken: 123, 281 | } 282 | Convey("Then MarshalBinary returns []byte{1, 0, 0, 3} as first 4 bytes", func() { 283 | b, err := p.MarshalBinary() 284 | So(err, ShouldBeNil) 285 | So(b[0:4], ShouldResemble, []byte{1, 0, 0, 3}) 286 | }) 287 | }) 288 | 289 | Convey("Given the slice []byte{2, 123, 0, 3, 123, 125}", func() { 290 | b := []byte{2, 123, 0, 3, 123, 125} 291 | Convey("Then UnmarshalBinary returns RandomToken=123", func() { 292 | err := p.UnmarshalBinary(b) 293 | So(err, ShouldBeNil) 294 | So(p, ShouldResemble, PullRespPacket{ 295 | ProtocolVersion: ProtocolVersion2, 296 | RandomToken: 123, 297 | }) 298 | }) 299 | }) 300 | }) 301 | } 302 | 303 | func TestTXACKPacket(t *testing.T) { 304 | Convey("Given an empty TXACKPacket", t, func() { 305 | var p TXACKPacket 306 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}", func() { 307 | b, err := p.MarshalBinary() 308 | So(err, ShouldBeNil) 309 | So(b, ShouldResemble, []byte{0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0}) 310 | }) 311 | 312 | Convey("Given ProtocolVersion=2, RandomToken=123 and GatewayMAC=[]byte{8, 7, 6, 5, 4, 3, 2, 1}", func() { 313 | p.ProtocolVersion = ProtocolVersion2 314 | p.RandomToken = 123 315 | p.GatewayMAC = [8]byte{8, 7, 6, 5, 4, 3, 2, 1} 316 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}", func() { 317 | b, err := p.MarshalBinary() 318 | So(err, ShouldBeNil) 319 | So(b, ShouldResemble, []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}) 320 | }) 321 | }) 322 | 323 | Convey("Given the slice []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1}", func() { 324 | b := []byte{2, 123, 0, 5, 8, 7, 6, 5, 4, 3, 2, 1} 325 | 326 | Convey("Then UnmarshalBinary return RandomToken=123 and GatewayMAC=[8]byte{8, 7, 6, 5, 4, 3, 2, 1}", func() { 327 | err := p.UnmarshalBinary(b) 328 | So(err, ShouldBeNil) 329 | So(p.RandomToken, ShouldEqual, 123) 330 | So(p.GatewayMAC[:], ShouldResemble, []byte{8, 7, 6, 5, 4, 3, 2, 1}) 331 | So(p.Payload, ShouldBeNil) 332 | So(p.ProtocolVersion, ShouldEqual, ProtocolVersion2) 333 | }) 334 | }) 335 | 336 | Convey("Given ProtocolVersion=2, RandomToken=123 and a payload with Error=COLLISION_BEACON", func() { 337 | p.ProtocolVersion = ProtocolVersion2 338 | p.RandomToken = 123 339 | p.Payload = &TXACKPayload{ 340 | TXPKACK: TXPKACK{ 341 | Error: "COLLISION_BEACON", 342 | }, 343 | } 344 | 345 | Convey("Then MarshalBinary returns []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}", func() { 346 | b, err := p.MarshalBinary() 347 | So(err, ShouldBeNil) 348 | So(b, ShouldResemble, []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}) 349 | }) 350 | }) 351 | 352 | Convey("Given the slice []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125}", func() { 353 | b := []byte{2, 123, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 123, 34, 116, 120, 112, 107, 95, 97, 99, 107, 34, 58, 123, 34, 101, 114, 114, 111, 114, 34, 58, 34, 67, 79, 76, 76, 73, 83, 73, 79, 78, 95, 66, 69, 65, 67, 79, 78, 34, 125, 125} 354 | Convey("Then UnmarshalBinary returns RandomToken=123 and a payload with Error=COLLISION_BEACON", func() { 355 | err := p.UnmarshalBinary(b) 356 | So(err, ShouldBeNil) 357 | So(p.RandomToken, ShouldEqual, 123) 358 | So(p.ProtocolVersion, ShouldEqual, ProtocolVersion2) 359 | So(p.Payload, ShouldResemble, &TXACKPayload{ 360 | TXPKACK: TXPKACK{ 361 | Error: "COLLISION_BEACON", 362 | }, 363 | }) 364 | }) 365 | }) 366 | }) 367 | } 368 | --------------------------------------------------------------------------------