├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile-devel ├── LICENSE ├── Makefile ├── README.md ├── airtime ├── airtime.go └── airtime_test.go ├── applayer ├── clocksync │ ├── cid_string.go │ ├── clocksync.go │ └── clocksync_test.go ├── firmwaremanagement │ ├── cid_string.go │ ├── firmwaremanagement.go │ └── firmwaremangement_test.go ├── fragmentation │ ├── cid_string.go │ ├── encode.go │ ├── encode_test.go │ ├── fragmentation.go │ └── fragmentation_test.go └── multicastsetup │ ├── cid_string.go │ ├── keys.go │ ├── keys_test.go │ ├── multicastsetup.go │ └── multicastsetup_test.go ├── backend ├── backend.go ├── backend_test.go ├── client.go ├── client_test.go └── joinserver │ ├── context.go │ ├── errors.go │ ├── join_request.go │ ├── joinserver.go │ ├── joinserver_test.go │ ├── rejoin_request.go │ └── session_keys.go ├── band ├── band.go ├── band_as923.go ├── band_as923_test.go ├── band_au915_928.go ├── band_au915_928_test.go ├── band_cn470_510.go ├── band_cn470_510_test.go ├── band_cn779_787.go ├── band_cn779_787_test.go ├── band_eu433.go ├── band_eu433_test.go ├── band_eu863_870.go ├── band_eu863_870_test.go ├── band_in_865_867.go ├── band_in_865_867_test.go ├── band_ism2400.go ├── band_ism2400_test.go ├── band_kr920_923.go ├── band_kr920_923_test.go ├── band_ru864_870.go ├── band_ru864_870_test.go ├── band_us902_928.go ├── band_us902_928_test.go └── errors.go ├── cid_string.go ├── devicemodeclass_string.go ├── doc.go ├── docker-compose.yml ├── eirp.go ├── eirp_test.go ├── fhdr.go ├── fhdr_test.go ├── go.mod ├── go.sum ├── gps ├── gps.go └── gps_test.go ├── jointype_string.go ├── mac_command_payload_test.go ├── mac_commands.go ├── mac_commands_test.go ├── macpayload.go ├── macpayload_test.go ├── major_string.go ├── mtype_string.go ├── netid.go ├── netid_test.go ├── payload.go ├── payload_test.go ├── phypayload.go ├── phypayload_test.go └── sensitivity ├── sensitivity.go └── sensitivity_test.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Run the tests 9 | run: docker-compose run --rm lorawan make test 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # hidden files 2 | .* 3 | 4 | -------------------------------------------------------------------------------- /Dockerfile-devel: -------------------------------------------------------------------------------- 1 | FROM golang:1.15-alpine 2 | 3 | ENV PROJECT_PATH=/lorawan 4 | ENV PATH=$PATH:$PROJECT_PATH/build 5 | ENV CGO_ENABLED=0 6 | ENV GO_EXTRA_BUILD_ARGS="-a -installsuffix cgo" 7 | 8 | RUN apk add --no-cache make git bash 9 | 10 | RUN mkdir -p $PROJECT_PATH 11 | COPY . $PROJECT_PATH 12 | WORKDIR $PROJECT_PATH 13 | 14 | RUN make dev-requirements 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test dev-requirements requirements 2 | 3 | lint: 4 | golint ./... 5 | go vet ./... 6 | 7 | test: lint 8 | go test -cover -v ./... 9 | 10 | dev-requirements: 11 | go mod download 12 | go install golang.org/x/lint/golint 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoRaWAN (Go) 2 | 3 | ![Tests](https://github.com/brocaar/lorawan/actions/workflows/main.yml/badge.svg?branch=master) 4 | [![GoDoc](https://godoc.org/github.com/brocaar/lorawan?status.svg)](https://godoc.org/github.com/brocaar/lorawan) 5 | 6 | Package lorawan provides structures and tools to read and write LoRaWAN 7 | 1.0 and 1.1 frames from and to a slice of bytes. 8 | 9 | The following structures are implemented (+ fields): 10 | 11 | ``` 12 | PHYPayload (MHDR | MACPayload | MIC) 13 | MACPayload (FHDR | FPort | FRMPayload) 14 | FHDR (DevAddr | FCtrl | FCnt | FOpts) 15 | ``` 16 | 17 | The Following message types (MType) are implemented: 18 | 19 | * JoinRequest 20 | * RejoinRequest 21 | * JoinAccept 22 | * UnconfirmedDataUp 23 | * UnconfirmedDataDown 24 | * ConfirmedDataUp 25 | * ConfirmedDataDown 26 | * Proprietary 27 | 28 | The following MAC commands (and their optional payloads) are implemented: 29 | 30 | * ResetInd 31 | * ResetConf 32 | * LinkCheckReq 33 | * LinkCheckAns 34 | * LinkADRReq 35 | * LinkADRAns 36 | * DutyCycleReq 37 | * DutyCycleAns 38 | * RXParamSetupReq 39 | * RXParamSetupAns 40 | * DevStatusReq 41 | * DevStatusAns 42 | * NewChannelReq 43 | * NewChannelAns 44 | * RXTimingSetupReq 45 | * RXTimingSetupAns 46 | * TXParamSetupReq 47 | * TXParamSetupAns 48 | * DLChannelReq 49 | * DLChannelAns 50 | * RekeyInd 51 | * RekeyConf 52 | * ADRParamSetupReq 53 | * ADRParamSetupAns 54 | * DeviceTimeReq 55 | * DeviceTimeAns 56 | * ForceRejoinReq 57 | * RejoinParamSetupReq 58 | * RejoinParamSetupAns 59 | * PingSlotInfoReq 60 | * PingSlotInfoAns 61 | * PingSlotChannelReq 62 | * PingSlotChannelAns 63 | * BeaconFreqReq 64 | * BeaconFreqAns 65 | * DeviceModeInd 66 | * DeviceModeConf 67 | * Proprietary commands (0x80 - 0xFF) can be registered with RegisterProprietaryMACCommand 68 | 69 | 70 | ## Sub-packages 71 | 72 | * `airtime` functions for calculating TX time-on-air 73 | * `band` ISM band configuration from the LoRaWAN Regional Parameters specification 74 | * `backend` Structs matching the LoRaWAN Backend Interface specification object 75 | * `backend/joinserver` LoRaWAN Backend Interface join-server interface implementation (`http.Handler`) 76 | * `applayer/clocksync` Application Layer Clock Synchronization over LoRaWAN 77 | * `applayer/multicastsetup` Application Layer Remote Multicast Setup over LoRaWAN 78 | * `applayer/fragmentation` Fragmented Data Block Transport over LoRaWAN 79 | * `applayer/firmwaremanagement` Firmware Management Protocol over LoRaWAN 80 | * `gps` functions to handle Time <> GPS Epoch time conversion 81 | 82 | ## Documentation 83 | 84 | See https://godoc.org/github.com/brocaar/lorawan. There is also an [examples](https://godoc.org/github.com/brocaar/lorawan#pkg-examples) 85 | section with usage examples. When using this package, knowledge about the LoRaWAN specification is needed. 86 | You can download the LoRaWAN specification here: https://lora-alliance.org/lorawan-for-developers 87 | 88 | ## Support 89 | 90 | For questions, feedback or support, please refer to the ChirpStack Community Forum: 91 | [https://forum.chirpstack.io](https://forum.chirpstack.io/). 92 | 93 | ## License 94 | 95 | This package is distributed under the MIT license which can be found in ``LICENSE``. 96 | LoRaWAN is a trademark of the LoRa Alliance Inc. (https://www.lora-alliance.org/). 97 | -------------------------------------------------------------------------------- /airtime/airtime.go: -------------------------------------------------------------------------------- 1 | // Package airtime provides a function for calculating the time on air. 2 | // This implements the formula as defined by: 3 | // https://www.semtech.com/uploads/documents/LoraDesignGuide_STD.pdf. 4 | package airtime 5 | 6 | import ( 7 | "errors" 8 | "math" 9 | "time" 10 | ) 11 | 12 | // CodingRate defines the coding-rate type. 13 | type CodingRate int 14 | 15 | // Available coding-rates. 16 | const ( 17 | CodingRate45 CodingRate = 1 18 | CodingRate46 CodingRate = 2 19 | CodingRate47 CodingRate = 3 20 | CodingRate48 CodingRate = 4 21 | ) 22 | 23 | // CalculateLoRaAirtime calculates the airtime for a LoRa modulated frame. 24 | func CalculateLoRaAirtime(payloadSize, sf, bandwidth, preambleNumber int, codingRate CodingRate, headerEnabled, lowDataRateOptimization bool) (time.Duration, error) { 25 | symbolDuration := CalculateLoRaSymbolDuration(sf, bandwidth) 26 | preambleDuration := CalculateLoRaPreambleDuration(symbolDuration, preambleNumber) 27 | 28 | payloadSymbolNumber, err := CalculateLoRaPayloadSymbolNumber(payloadSize, sf, codingRate, headerEnabled, lowDataRateOptimization) 29 | if err != nil { 30 | return 0, err 31 | } 32 | 33 | return preambleDuration + (time.Duration(payloadSymbolNumber) * symbolDuration), nil 34 | } 35 | 36 | // CalculateLoRaSymbolDuration calculates the LoRa symbol duration. 37 | func CalculateLoRaSymbolDuration(sf int, bandwidth int) time.Duration { 38 | return time.Duration((1 << uint(sf)) * 1000000 / bandwidth) 39 | } 40 | 41 | // CalculateLoRaPreambleDuration calculates the LoRa preamble duration. 42 | func CalculateLoRaPreambleDuration(symbolDuration time.Duration, preambleNumber int) time.Duration { 43 | return time.Duration((100*preambleNumber)+425) * symbolDuration / 100 44 | } 45 | 46 | // CalculateLoRaPayloadSymbolNumber returns the number of symbols that make 47 | // up the packet payload and header. 48 | func CalculateLoRaPayloadSymbolNumber(payloadSize, sf int, codingRate CodingRate, headerEnabled, lowDataRateOptimization bool) (int, error) { 49 | var pl, spreadingFactor, h, de, cr float64 50 | 51 | if codingRate < 1 || codingRate > 4 { 52 | return 0, errors.New("codingRate must be between 1 - 4") 53 | } 54 | 55 | if lowDataRateOptimization { 56 | de = 1 57 | } 58 | 59 | if !headerEnabled { 60 | h = 1 61 | } 62 | 63 | pl = float64(payloadSize) 64 | spreadingFactor = float64(sf) 65 | cr = float64(codingRate) 66 | 67 | a := 8*pl - 4*spreadingFactor + 28 + 16 - 20*h 68 | b := 4 * (spreadingFactor - 2*de) 69 | c := cr + 4 70 | 71 | return int(8 + math.Max(math.Ceil(a/b)*c, 0)), nil 72 | } 73 | -------------------------------------------------------------------------------- /airtime/airtime_test.go: -------------------------------------------------------------------------------- 1 | package airtime 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestCalculateLoRaAirtime(t *testing.T) { 12 | tests := []struct { 13 | PayloadSize int 14 | SF int 15 | Bandwidth int 16 | PreambleNum int 17 | CodingRate CodingRate 18 | HeaderEnabled bool 19 | LowDataRateOptimization bool 20 | ExpectedAirtime time.Duration 21 | }{ 22 | { 23 | PayloadSize: 13, 24 | SF: 12, 25 | Bandwidth: 125, 26 | PreambleNum: 8, 27 | CodingRate: CodingRate45, 28 | HeaderEnabled: true, 29 | LowDataRateOptimization: false, 30 | ExpectedAirtime: time.Duration(1155072 * 1000), 31 | }, 32 | } 33 | 34 | Convey("Given a test-table", t, func() { 35 | for i, test := range tests { 36 | Convey(fmt.Sprintf("Test: %d", i), func() { 37 | d, err := CalculateLoRaAirtime(test.PayloadSize, test.SF, test.Bandwidth, test.PreambleNum, test.CodingRate, test.HeaderEnabled, test.LowDataRateOptimization) 38 | So(err, ShouldBeNil) 39 | So(d, ShouldEqual, test.ExpectedAirtime) 40 | }) 41 | } 42 | }) 43 | } 44 | 45 | func TestCalculateLoRaSymbolDuration(t *testing.T) { 46 | tests := []struct { 47 | SF int 48 | Bandwidth int 49 | ExpectedDuration time.Duration 50 | }{ 51 | { 52 | SF: 12, 53 | Bandwidth: 125, 54 | ExpectedDuration: time.Duration(32768 * 1000), 55 | }, 56 | { 57 | SF: 9, 58 | Bandwidth: 125, 59 | ExpectedDuration: time.Duration(4096 * 1000), 60 | }, 61 | { 62 | SF: 9, 63 | Bandwidth: 500, 64 | ExpectedDuration: time.Duration(1024 * 1000), 65 | }, 66 | } 67 | 68 | Convey("Given a test-table", t, func() { 69 | for i, test := range tests { 70 | Convey(fmt.Sprintf("Test: %d", i), func() { 71 | So(CalculateLoRaSymbolDuration(test.SF, test.Bandwidth), ShouldEqual, test.ExpectedDuration) 72 | }) 73 | } 74 | }) 75 | } 76 | 77 | func TestCalculateLoRaPreambleDuration(t *testing.T) { 78 | Convey("Given a test-table", t, func() { 79 | tests := []struct { 80 | SymbolDuration time.Duration 81 | PreambleNumber int 82 | ExpectedDuration time.Duration 83 | }{ 84 | { 85 | SymbolDuration: CalculateLoRaSymbolDuration(12, 125), 86 | PreambleNumber: 8, 87 | ExpectedDuration: time.Duration(401408 * 1000), 88 | }, 89 | } 90 | 91 | for i, test := range tests { 92 | Convey(fmt.Sprintf("Test: %d", i), func() { 93 | So(CalculateLoRaPreambleDuration(test.SymbolDuration, test.PreambleNumber), ShouldEqual, test.ExpectedDuration) 94 | }) 95 | } 96 | }) 97 | } 98 | 99 | func TestCalculateLoRaPayloadSymbolNumber(t *testing.T) { 100 | Convey("Given a test-table", t, func() { 101 | tests := []struct { 102 | PayloadSize int 103 | SF int 104 | CodingRate CodingRate 105 | HeaderEnabled bool 106 | LowDataRateOptimization bool 107 | ExpectedNumber int 108 | }{ 109 | { 110 | PayloadSize: 13, 111 | SF: 12, 112 | CodingRate: CodingRate45, 113 | HeaderEnabled: true, 114 | LowDataRateOptimization: false, 115 | ExpectedNumber: 23, 116 | }, 117 | { 118 | PayloadSize: 13, 119 | SF: 12, 120 | CodingRate: CodingRate46, 121 | HeaderEnabled: true, 122 | LowDataRateOptimization: false, 123 | ExpectedNumber: 26, 124 | }, 125 | { 126 | PayloadSize: 13, 127 | SF: 12, 128 | CodingRate: CodingRate45, 129 | HeaderEnabled: false, 130 | LowDataRateOptimization: false, 131 | ExpectedNumber: 18, 132 | }, 133 | 134 | { 135 | PayloadSize: 50, 136 | SF: 12, 137 | CodingRate: CodingRate45, 138 | HeaderEnabled: true, 139 | LowDataRateOptimization: true, 140 | ExpectedNumber: 58, 141 | }, 142 | } 143 | 144 | for i, test := range tests { 145 | Convey(fmt.Sprintf("Test: %d", i), func() { 146 | num, err := CalculateLoRaPayloadSymbolNumber(test.PayloadSize, test.SF, test.CodingRate, test.HeaderEnabled, test.LowDataRateOptimization) 147 | So(err, ShouldBeNil) 148 | So(num, ShouldEqual, test.ExpectedNumber) 149 | }) 150 | } 151 | }) 152 | 153 | } 154 | -------------------------------------------------------------------------------- /applayer/clocksync/cid_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=CID"; DO NOT EDIT. 2 | 3 | package clocksync 4 | 5 | import "strconv" 6 | 7 | const _CID_name = "PackageVersionReqAppTimeReqDeviceAppTimePeriodicityReqForceDeviceResyncReq" 8 | 9 | var _CID_index = [...]uint8{0, 17, 27, 54, 74} 10 | 11 | func (i CID) String() string { 12 | if i >= CID(len(_CID_index)-1) { 13 | return "CID(" + strconv.FormatInt(int64(i), 10) + ")" 14 | } 15 | return _CID_name[_CID_index[i]:_CID_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /applayer/clocksync/clocksync_test.go: -------------------------------------------------------------------------------- 1 | package clocksync 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestClockSync(t *testing.T) { 11 | tests := []struct { 12 | Name string 13 | Command Command 14 | Bytes []byte 15 | Uplink bool 16 | ExpectedMarshalError error 17 | ExpectedUnmarshalError error 18 | }{ 19 | { 20 | Name: "PackageVersionReq", 21 | Command: Command{ 22 | CID: PackageVersionReq, 23 | }, 24 | Bytes: []byte{0x00}, 25 | }, 26 | { 27 | Name: "PackageVersionAns", 28 | Command: Command{ 29 | CID: PackageVersionAns, 30 | Payload: &PackageVersionAnsPayload{ 31 | PackageIdentifier: 1, 32 | PackageVersion: 1, 33 | }, 34 | }, 35 | Uplink: true, 36 | Bytes: []byte{0x00, 0x01, 0x01}, 37 | }, 38 | { 39 | Name: "PackageVersionAns invalid bytes", 40 | Uplink: true, 41 | Bytes: []byte{0x00, 0x01}, 42 | ExpectedUnmarshalError: errors.New("lorawan/applayer/clocksync: 2 bytes are expected"), 43 | }, 44 | { 45 | Name: "AppTimeReq", 46 | Uplink: true, 47 | Command: Command{ 48 | CID: AppTimeReq, 49 | Payload: &AppTimeReqPayload{ 50 | DeviceTime: 134480385, 51 | Param: AppTimeReqPayloadParam{ 52 | TokenReq: 5, 53 | AnsRequired: true, 54 | }, 55 | }, 56 | }, 57 | Bytes: []byte{0x01, 0x01, 0x02, 0x04, 0x08, 0x15}, 58 | }, 59 | { 60 | Name: "AppTimeReq invalid bytes", 61 | Uplink: true, 62 | Bytes: []byte{0x01, 0x01, 0x02, 0x04, 0x08}, 63 | ExpectedUnmarshalError: errors.New("lorawan/applayer/clocksync: 5 bytes are expected"), 64 | }, 65 | { 66 | Name: "AppTimeAns", 67 | Command: Command{ 68 | CID: AppTimeAns, 69 | Payload: &AppTimeAnsPayload{ 70 | TimeCorrection: -134480385, 71 | Param: AppTimeAnsPayloadParam{ 72 | TokenAns: 5, 73 | }, 74 | }, 75 | }, 76 | Bytes: []byte{0x01, 0xff, 0xfd, 0xfb, 0xf7, 0x05}, 77 | }, 78 | { 79 | Name: "AppTimeAns invalid bytes", 80 | Bytes: []byte{0x01, 0x01, 0x02, 0x04, 0x08}, 81 | ExpectedUnmarshalError: errors.New("lorawan/applayer/clocksync: 5 bytes are expected"), 82 | }, 83 | { 84 | Name: "DeviceAppTimePeriodicityReq", 85 | Command: Command{ 86 | CID: DeviceAppTimePeriodicityReq, 87 | Payload: &DeviceAppTimePeriodicityReqPayload{ 88 | Periodicity: DeviceAppTimePeriodicityReqPayloadPeriodicity{ 89 | 5, 90 | }, 91 | }, 92 | }, 93 | Bytes: []byte{0x02, 0x05}, 94 | }, 95 | { 96 | Name: "DeviceAppTimePeriodicityReq invalid bytes", 97 | Bytes: []byte{0x02}, 98 | ExpectedUnmarshalError: errors.New("lorawan/applayer/clocksync: 1 bytes are expected"), 99 | }, 100 | { 101 | Name: "DeviceAppTimePeriodicityAns", 102 | Uplink: true, 103 | Command: Command{ 104 | CID: DeviceAppTimePeriodicityAns, 105 | Payload: &DeviceAppTimePeriodicityAnsPayload{ 106 | Status: DeviceAppTimePeriodicityAnsPayloadStatus{ 107 | NotSupported: true, 108 | }, 109 | Time: 134480385, 110 | }, 111 | }, 112 | Bytes: []byte{0x02, 0x01, 0x01, 0x02, 0x04, 0x08}, 113 | }, 114 | { 115 | Name: "DeviceAppTimePeriodicityAns invalid bytes", 116 | Uplink: true, 117 | Bytes: []byte{0x02, 0x01, 0x01, 0x02, 0x04}, 118 | ExpectedUnmarshalError: errors.New("lorawan/applayer/clocksync: 5 bytes are expected"), 119 | }, 120 | { 121 | Name: "ForceDeviceResyncReq", 122 | Command: Command{ 123 | CID: ForceDeviceResyncReq, 124 | Payload: &ForceDeviceResyncReqPayload{ 125 | ForceConf: ForceDeviceResyncReqPayloadForceConf{ 126 | NbTransmissions: 5, 127 | }, 128 | }, 129 | }, 130 | Bytes: []byte{0x03, 0x05}, 131 | }, 132 | { 133 | Name: "ForceDeviceResyncReq invalid bytes", 134 | Bytes: []byte{0x03}, 135 | ExpectedUnmarshalError: errors.New("lorawan/applayer/clocksync: 1 bytes are expected"), 136 | }, 137 | } 138 | 139 | for _, tst := range tests { 140 | t.Run(tst.Name, func(t *testing.T) { 141 | assert := require.New(t) 142 | 143 | if tst.ExpectedMarshalError != nil { 144 | _, err := tst.Command.MarshalBinary() 145 | assert.Equal(tst.ExpectedMarshalError, err) 146 | } else if tst.ExpectedUnmarshalError != nil { 147 | var cmd Command 148 | err := cmd.UnmarshalBinary(tst.Uplink, tst.Bytes) 149 | assert.Equal(tst.ExpectedUnmarshalError, err) 150 | } else { 151 | cmds := Commands{tst.Command} 152 | b, err := cmds.MarshalBinary() 153 | assert.NoError(err) 154 | assert.Equal(tst.Bytes, b) 155 | 156 | cmds = Commands{} 157 | assert.NoError(cmds.UnmarshalBinary(tst.Uplink, tst.Bytes)) 158 | assert.Len(cmds, 1) 159 | assert.Equal(tst.Command, cmds[0]) 160 | } 161 | }) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /applayer/firmwaremanagement/cid_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=CID"; DO NOT EDIT. 2 | 3 | package firmwaremanagement 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[PackageVersionReq-0] 12 | _ = x[PackageVersionAns-0] 13 | _ = x[DevVersionReq-1] 14 | _ = x[DevVersionAns-1] 15 | _ = x[DevRebootTimeReq-2] 16 | _ = x[DevRebootTimeAns-2] 17 | _ = x[DevRebootCountdownReq-3] 18 | _ = x[DevRebootCountdownAns-3] 19 | _ = x[DevUpgradeImageReq-4] 20 | _ = x[DevUpgradeImageAns-4] 21 | _ = x[DevDeleteImageReq-5] 22 | _ = x[DevDeleteImageAns-5] 23 | } 24 | 25 | const _CID_name = "PackageVersionReqDevVersionReqDevRebootTimeReqDevRebootCountdownReqDevUpgradeImageReqDevDeleteImageReq" 26 | 27 | var _CID_index = [...]uint8{0, 17, 30, 46, 67, 85, 102} 28 | 29 | func (i CID) String() string { 30 | if i >= CID(len(_CID_index)-1) { 31 | return "CID(" + strconv.FormatInt(int64(i), 10) + ")" 32 | } 33 | return _CID_name[_CID_index[i]:_CID_index[i+1]] 34 | } 35 | -------------------------------------------------------------------------------- /applayer/firmwaremanagement/firmwaremangement_test.go: -------------------------------------------------------------------------------- 1 | package firmwaremanagement 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFirmwareManagement(t *testing.T) { 11 | nextFirmwareVersion := uint32(262657) 12 | 13 | tests := []struct { 14 | Name string 15 | Command Command 16 | Bytes []byte 17 | Uplink bool 18 | ExpectedMarshalError error 19 | ExpectedUnmarshalError error 20 | }{ 21 | { 22 | Name: "PackageVersionReq", 23 | Command: Command{ 24 | CID: PackageVersionReq, 25 | }, 26 | Bytes: []byte{0x00}, 27 | }, 28 | { 29 | Name: "PackageVersionAns", 30 | Uplink: true, 31 | Command: Command{ 32 | CID: PackageVersionAns, 33 | Payload: &PackageVersionAnsPayload{ 34 | PackageIdentifier: 1, 35 | PackageVersion: 1, 36 | }, 37 | }, 38 | Bytes: []byte{0x00, 0x01, 0x01}, 39 | }, 40 | { 41 | Name: "PackageVersionAns invalid bytes", 42 | Uplink: true, 43 | Bytes: []byte{0x00, 0x01}, 44 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 2 bytes are expected"), 45 | }, 46 | { 47 | Name: "DevVersionReq", 48 | Command: Command{ 49 | CID: DevVersionReq, 50 | Payload: &DevVersionReqPayload{}, 51 | }, 52 | Bytes: []byte{0x01}, 53 | }, 54 | { 55 | Name: "DevVersionReq invalid bytes", 56 | Bytes: []byte{0x01, 0x2}, 57 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 0 bytes are expected"), 58 | }, 59 | { 60 | Name: "DevVersionAns", 61 | Uplink: true, 62 | Command: Command{ 63 | CID: DevVersionAns, 64 | Payload: &DevVersionAnsPayload{ 65 | FWversion: 513, 66 | HWversion: 1264, 67 | }, 68 | }, 69 | Bytes: []byte{0x01, 0x01, 0x02, 0x00, 0x00, 0xF0, 0x04, 0x00, 0x00}, 70 | }, 71 | { 72 | Name: "DevVersionAns invalid bytes", 73 | Uplink: true, 74 | Bytes: []byte{0x01, 0x02, 0x01, 0x04}, 75 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 8 bytes are expected"), 76 | }, 77 | { 78 | Name: "DevRebootTimeReq", 79 | Command: Command{ 80 | CID: DevRebootTimeReq, 81 | Payload: &DevRebootTimeReqPayload{ 82 | RebootTime: 134480385, 83 | }, 84 | }, 85 | Bytes: []byte{0x02, 0x01, 0x02, 0x04, 0x08}, 86 | }, 87 | { 88 | Name: "DevRebootTimeReq invalid bytes", 89 | Bytes: []byte{0x02, 0x01, 0x02, 0x04}, 90 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 4 bytes are expected"), 91 | }, 92 | { 93 | Name: "DevRebootTimeAns", 94 | Uplink: true, 95 | Command: Command{ 96 | CID: DevRebootTimeAns, 97 | Payload: &DevRebootTimeAnsPayload{ 98 | RebootTime: 134480385, 99 | }, 100 | }, 101 | Bytes: []byte{0x02, 0x01, 0x02, 0x04, 0x08}, 102 | }, 103 | { 104 | Name: "DevRebootTimeAns invalid bytes", 105 | Uplink: true, 106 | Bytes: []byte{0x02, 0x01, 0x02, 0x04}, 107 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 4 bytes are expected"), 108 | }, 109 | { 110 | Name: "DevRebootCountdownReq", 111 | Command: Command{ 112 | CID: DevRebootCountdownReq, 113 | Payload: &DevRebootCountdownReqPayload{ 114 | Countdown: 262657, 115 | }, 116 | }, 117 | Bytes: []byte{0x03, 0x01, 0x02, 0x04}, 118 | }, 119 | { 120 | Name: "DevRebootCountdownReq invalid bytes", 121 | Bytes: []byte{0x03, 0x01, 0x02}, 122 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 3 bytes are expected"), 123 | }, 124 | { 125 | Name: "DevRebootCountdownAns", 126 | Uplink: true, 127 | Command: Command{ 128 | CID: DevRebootCountdownAns, 129 | Payload: &DevRebootCountdownAnsPayload{ 130 | Countdown: 262657, 131 | }, 132 | }, 133 | Bytes: []byte{0x03, 0x01, 0x02, 0x04}, 134 | }, 135 | { 136 | Name: "DevRebootCountdownAns invalid bytes", 137 | Uplink: true, 138 | Bytes: []byte{0x03, 0x01, 0x02}, 139 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 3 bytes are expected"), 140 | }, 141 | { 142 | Name: "DevUpgradeImageReq", 143 | Command: Command{ 144 | CID: DevUpgradeImageReq, 145 | Payload: &DevUpgradeImageReqPayload{}, 146 | }, 147 | Bytes: []byte{0x04}, 148 | }, 149 | { 150 | Name: "DevUpgradeImageAns Valid Firmware", 151 | Uplink: true, 152 | Command: Command{ 153 | CID: DevUpgradeImageAns, 154 | Payload: &DevUpgradeImageAnsPayload{ 155 | Status: DevUpgradeImageAnsPayloadStatus{ 156 | UpImageStatus: FirmwareValid, 157 | }, 158 | nextFirmwareVersion: &nextFirmwareVersion, 159 | }, 160 | }, 161 | Bytes: []byte{0x04, 0x03, 0x01, 0x02, 0x04, 0x00}, 162 | }, 163 | { 164 | Name: "DevUpgradeImageAns Invalid Firmware", 165 | Uplink: true, 166 | Command: Command{ 167 | CID: DevUpgradeImageAns, 168 | Payload: &DevUpgradeImageAnsPayload{ 169 | Status: DevUpgradeImageAnsPayloadStatus{ 170 | UpImageStatus: FirmwareCorruptOrInvalidSignature, 171 | }, 172 | }, 173 | }, 174 | Bytes: []byte{0x04, 0x01}}, 175 | 176 | { 177 | Name: "DevUpgradeImageAns InvalidFirmware with nextFirmwareVersion", 178 | Uplink: true, 179 | Command: Command{ 180 | CID: DevUpgradeImageAns, 181 | Payload: &DevUpgradeImageAnsPayload{ 182 | Status: DevUpgradeImageAnsPayloadStatus{ 183 | UpImageStatus: FirmwareCorruptOrInvalidSignature, 184 | }, 185 | nextFirmwareVersion: &nextFirmwareVersion, 186 | }, 187 | }, 188 | ExpectedMarshalError: errors.New("lorawan/applayer/firmwaremanagement: nextFirmwareVersion must be nil when UpImageStatus != 3 due no valid firmware present"), 189 | }, 190 | { 191 | Name: "DevVersionAns invalid bytes - Valid Firware in UplinkStatus", 192 | Uplink: true, 193 | Bytes: []byte{0x04, 0x03, 0x01, 0x02, 0x03}, 194 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 5 bytes are expected"), 195 | }, 196 | { 197 | Name: "DevVersionAns invalid bytes - no UplinkStatus", 198 | Uplink: true, 199 | Bytes: []byte{0x04}, 200 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: at least 1 byte is expected"), 201 | }, 202 | { 203 | Name: "DevDeleteImageReq", 204 | Command: Command{ 205 | CID: DevDeleteImageReq, 206 | Payload: &DevDeleteImageReqPayload{ 207 | FirmwareToDeleteVersion: 197121, 208 | }, 209 | }, 210 | Bytes: []byte{0x05, 0x01, 0x02, 0x03, 0x00}, 211 | }, 212 | { 213 | Name: "DevVersionReq invalid bytes", 214 | Bytes: []byte{0x05, 0x01, 0x02, 0x0}, 215 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 4 bytes are expected"), 216 | }, 217 | { 218 | Name: "DevDeleteImageAns", 219 | Uplink: true, 220 | Command: Command{ 221 | CID: DevDeleteImageAns, 222 | Payload: &DevDeleteImageAnsPayload{ 223 | Status: DevDeleteImageAnsPayloadStatus{ 224 | ErrorInvalidVersion: 1, 225 | ErrorNoValidImage: 1, 226 | }, 227 | }, 228 | }, 229 | Bytes: []byte{0x05, 0x3}, 230 | }, 231 | { 232 | Name: "DevVersionAns invalid bytes", 233 | Uplink: true, 234 | Bytes: []byte{0x05}, 235 | ExpectedUnmarshalError: errors.New("lorawan/applayer/firmwaremanagement: 1 bytes are expected"), 236 | }, 237 | } 238 | 239 | for _, tst := range tests { 240 | t.Run(tst.Name, func(t *testing.T) { 241 | assert := require.New(t) 242 | 243 | if tst.ExpectedMarshalError != nil { 244 | _, err := tst.Command.MarshalBinary() 245 | assert.Equal(tst.ExpectedMarshalError, err) 246 | } else if tst.ExpectedUnmarshalError != nil { 247 | var cmd Command 248 | err := cmd.UnmarshalBinary(tst.Uplink, tst.Bytes) 249 | assert.Equal(tst.ExpectedUnmarshalError, err) 250 | } else { 251 | cmds := Commands{tst.Command} 252 | b, err := cmds.MarshalBinary() 253 | assert.NoError(err) 254 | assert.Equal(tst.Bytes, b) 255 | 256 | cmds = Commands{} 257 | assert.NoError(cmds.UnmarshalBinary(tst.Uplink, tst.Bytes)) 258 | assert.Len(cmds, 1) 259 | assert.Equal(tst.Command, cmds[0]) 260 | } 261 | }) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /applayer/fragmentation/cid_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=CID"; DO NOT EDIT. 2 | 3 | package fragmentation 4 | 5 | import "strconv" 6 | 7 | const ( 8 | _CID_name_0 = "PackageVersionReqFragSessionStatusReqFragSessionSetupReqFragSessionDeleteReq" 9 | _CID_name_1 = "DataFragment" 10 | ) 11 | 12 | var ( 13 | _CID_index_0 = [...]uint8{0, 17, 37, 56, 76} 14 | ) 15 | 16 | func (i CID) String() string { 17 | switch { 18 | case 0 <= i && i <= 3: 19 | return _CID_name_0[_CID_index_0[i]:_CID_index_0[i+1]] 20 | case i == 8: 21 | return _CID_name_1 22 | default: 23 | return "CID(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /applayer/fragmentation/encode.go: -------------------------------------------------------------------------------- 1 | package fragmentation 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Encode encodes the given slice of bytes to fragments including forward error correction. 8 | // This is based on the proposed FEC code from the Fragmented Data Block Transport over 9 | // LoRaWAN recommendation. 10 | func Encode(data []byte, fragmentSize, redundancy int) ([][]byte, error) { 11 | if len(data)%fragmentSize != 0 { 12 | return nil, errors.New("length of data must be a multiple of the given fragment-size") 13 | } 14 | 15 | // fragment the data into rows 16 | var dataRows [][]byte 17 | for i := 0; i < len(data)/fragmentSize; i++ { 18 | offset := i * fragmentSize 19 | dataRows = append(dataRows, data[offset:offset+fragmentSize]) 20 | } 21 | w := len(dataRows) 22 | 23 | for y := 0; y < redundancy; y++ { 24 | s := make([]byte, fragmentSize) 25 | a := matrixLine(y+1, w) 26 | 27 | for x := 0; x < w; x++ { 28 | if a[x] == 1 { 29 | for m := 0; m < fragmentSize; m++ { 30 | s[m] ^= dataRows[x][m] 31 | } 32 | } 33 | } 34 | 35 | dataRows = append(dataRows, s) 36 | } 37 | 38 | return dataRows, nil 39 | } 40 | 41 | func prbs23(x int) int { 42 | b0 := x & 1 43 | b1 := (x & 32) / 32 44 | return (x / 2) + (b0^b1)*(1<<22) 45 | } 46 | 47 | func isPower2(num int) bool { 48 | return num != 0 && (num&(num-1)) == 0 49 | } 50 | 51 | func matrixLine(n, m int) []int { 52 | line := make([]int, m) 53 | 54 | mm := 0 55 | if isPower2(m) { 56 | mm = 1 57 | } 58 | 59 | x := 1 + (1001 * n) 60 | 61 | for nbCoeff := 0; nbCoeff < m/2; nbCoeff++ { 62 | r := 1 << 16 63 | for r >= m { 64 | x = prbs23(x) 65 | r = x % (m + mm) 66 | } 67 | line[r] = 1 68 | } 69 | 70 | return line 71 | } 72 | -------------------------------------------------------------------------------- /applayer/fragmentation/encode_test.go: -------------------------------------------------------------------------------- 1 | package fragmentation 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEncode(t *testing.T) { 11 | data := make([]byte, 100) 12 | for i := range data { 13 | data[i] = byte(i) 14 | } 15 | 16 | tests := []struct { 17 | Name string 18 | Data []byte 19 | FragmentSize int 20 | Redundancy int 21 | ExpectedFragments [][]byte 22 | ExpectedError error 23 | }{ 24 | { 25 | Name: "invalid fragment size", 26 | Data: make([]byte, 5), 27 | FragmentSize: 10, 28 | ExpectedError: errors.New("length of data must be a multiple of the given fragment-size"), 29 | }, 30 | { 31 | Name: "fragment size 10, redundancy 10", 32 | Data: data, 33 | FragmentSize: 10, 34 | Redundancy: 10, 35 | ExpectedFragments: [][]byte{ 36 | []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9}, 37 | []byte{0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13}, 38 | []byte{0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d}, 39 | []byte{0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}, 40 | []byte{0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31}, 41 | []byte{0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b}, 42 | []byte{0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45}, 43 | []byte{0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f}, 44 | []byte{0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59}, 45 | []byte{0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63}, 46 | []byte{0x26, 0x26, 0x22, 0x22, 0x2e, 0x2e, 0x22, 0x22, 0x26, 0x26}, 47 | []byte{0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x6a, 0x6b, 0x7c, 0x7d}, 48 | []byte{0x5c, 0x5d, 0x6e, 0x6f, 0x10, 0x11, 0x2, 0x3, 0x4, 0x5}, 49 | []byte{0x36, 0x36, 0x32, 0x32, 0x3e, 0x3e, 0x22, 0x22, 0x36, 0x36}, 50 | []byte{0x3a, 0x3a, 0xe, 0xe, 0xa, 0xa, 0x6, 0x6, 0xa, 0xa}, 51 | []byte{0xe, 0xe, 0x32, 0x32, 0x36, 0x36, 0x22, 0x22, 0x3e, 0x3e}, 52 | []byte{0x2, 0x2, 0xe, 0xe, 0x72, 0x72, 0x76, 0x76, 0x62, 0x62}, 53 | []byte{0x1e, 0x1e, 0x1a, 0x1a, 0x66, 0x66, 0x5a, 0x5a, 0x4e, 0x4e}, 54 | []byte{0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x30, 0x31, 0x22, 0x23}, 55 | []byte{0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x18, 0x19, 0xa, 0xb}}, 56 | }, 57 | { 58 | Name: "fragment size 10, redundancy 5", 59 | Data: data, 60 | FragmentSize: 10, 61 | Redundancy: 5, 62 | ExpectedFragments: [][]byte{ 63 | []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9}, 64 | []byte{0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13}, 65 | []byte{0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d}, 66 | []byte{0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}, 67 | []byte{0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31}, 68 | []byte{0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b}, 69 | []byte{0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45}, 70 | []byte{0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f}, 71 | []byte{0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59}, 72 | []byte{0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63}, 73 | []byte{0x26, 0x26, 0x22, 0x22, 0x2e, 0x2e, 0x22, 0x22, 0x26, 0x26}, 74 | []byte{0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x6a, 0x6b, 0x7c, 0x7d}, 75 | []byte{0x5c, 0x5d, 0x6e, 0x6f, 0x10, 0x11, 0x2, 0x3, 0x4, 0x5}, 76 | []byte{0x36, 0x36, 0x32, 0x32, 0x3e, 0x3e, 0x22, 0x22, 0x36, 0x36}, 77 | []byte{0x3a, 0x3a, 0xe, 0xe, 0xa, 0xa, 0x6, 0x6, 0xa, 0xa}}, 78 | }, 79 | } 80 | 81 | for _, tst := range tests { 82 | t.Run(tst.Name, func(t *testing.T) { 83 | assert := require.New(t) 84 | 85 | fragments, err := Encode(tst.Data, tst.FragmentSize, tst.Redundancy) 86 | if tst.ExpectedError != nil { 87 | assert.Equal(tst.ExpectedError, err) 88 | return 89 | } 90 | 91 | assert.NoError(err) 92 | assert.Equal(tst.ExpectedFragments, fragments) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /applayer/fragmentation/fragmentation_test.go: -------------------------------------------------------------------------------- 1 | package fragmentation 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestFragmentation(t *testing.T) { 11 | tests := []struct { 12 | Name string 13 | Command Command 14 | Bytes []byte 15 | Uplink bool 16 | ExpectedMarshalError error 17 | ExpectedUnmarshalError error 18 | }{ 19 | { 20 | Name: "PackageVersionReq", 21 | Command: Command{ 22 | CID: PackageVersionReq, 23 | }, 24 | Bytes: []byte{0x00}, 25 | }, 26 | { 27 | Name: "PackageVersionAns", 28 | Uplink: true, 29 | Command: Command{ 30 | CID: PackageVersionAns, 31 | Payload: &PackageVersionAnsPayload{ 32 | PackageIdentifier: 1, 33 | PackageVersion: 1, 34 | }, 35 | }, 36 | Bytes: []byte{0x00, 0x01, 0x01}, 37 | }, 38 | { 39 | Name: "PackageVersionAns invalid bytes", 40 | Uplink: true, 41 | Bytes: []byte{0x00, 0x01}, 42 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 2 bytes are expected"), 43 | }, 44 | { 45 | Name: "FragSessionSetupReq", 46 | Command: Command{ 47 | CID: FragSessionSetupReq, 48 | Payload: &FragSessionSetupReqPayload{ 49 | FragSession: FragSessionSetupReqPayloadFragSession{ 50 | FragIndex: 3, 51 | McGroupBitMask: [4]bool{true, false, true, false}, 52 | }, 53 | NbFrag: 513, 54 | FragSize: 255, 55 | Control: FragSessionSetupReqPayloadControl{ 56 | FragmentationMatrix: 5, 57 | BlockAckDelay: 4, 58 | }, 59 | Padding: 129, 60 | Descriptor: [4]byte{0x01, 0x02, 0x03, 0x04}, 61 | }, 62 | }, 63 | Bytes: []byte{0x02, 0x35, 0x01, 0x02, 0xff, 0x2c, 0x81, 0x01, 0x02, 0x03, 0x04}, 64 | }, 65 | { 66 | Name: "FragSessionSetupReq invalid bytes", 67 | Bytes: []byte{0x02, 0x35, 0x01, 0x02, 0xff, 0x2c, 0x81, 0x01, 0x02, 0x03}, 68 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 10 bytes are expected"), 69 | }, 70 | { 71 | Name: "FragSessionSetupAns", 72 | Uplink: true, 73 | Command: Command{ 74 | CID: FragSessionSetupAns, 75 | Payload: &FragSessionSetupAnsPayload{ 76 | StatusBitMask: FragSessionSetupAnsPayloadStatusBitMask{ 77 | FragIndex: 3, 78 | WrongDescriptor: true, 79 | FragSessionIndexNotSupported: true, 80 | NotEnoughMemory: true, 81 | EncodingUnsupported: true, 82 | }, 83 | }, 84 | }, 85 | Bytes: []byte{0x02, 0xcf}, 86 | }, 87 | { 88 | Name: "FragSessionSetupAns invalid bytes", 89 | Uplink: true, 90 | Bytes: []byte{0x02}, 91 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 1 byte is expected"), 92 | }, 93 | { 94 | Name: "FragSessionDeleteReq", 95 | Command: Command{ 96 | CID: FragSessionDeleteReq, 97 | Payload: &FragSessionDeleteReqPayload{ 98 | Param: FragSessionDeleteReqPayloadParam{ 99 | FragIndex: 3, 100 | }, 101 | }, 102 | }, 103 | Bytes: []byte{0x03, 0x03}, 104 | }, 105 | { 106 | Name: "FragSessionDeleteReq invalid bytes", 107 | Bytes: []byte{0x03}, 108 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 1 byte is expected"), 109 | }, 110 | { 111 | Name: "FragSessionDeleteAns", 112 | Uplink: true, 113 | Command: Command{ 114 | CID: FragSessionDeleteAns, 115 | Payload: &FragSessionDeleteAnsPayload{ 116 | Status: FragSessionDeleteAnsPayloadStatus{ 117 | FragIndex: 3, 118 | SessionDoesNotExist: true, 119 | }, 120 | }, 121 | }, 122 | Bytes: []byte{0x03, 0x07}, 123 | }, 124 | { 125 | Name: "FragSessionDeleteAns invalid bytes", 126 | Uplink: true, 127 | Bytes: []byte{0x03}, 128 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 1 byte is expected"), 129 | }, 130 | { 131 | Name: "DataFragment", 132 | Command: Command{ 133 | CID: DataFragment, 134 | Payload: &DataFragmentPayload{ 135 | IndexAndN: DataFragmentPayloadIndexAndN{ 136 | FragIndex: 3, 137 | N: 513, 138 | }, 139 | Payload: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, 140 | }, 141 | }, 142 | Bytes: []byte{0x08, 0x01, 0xc2, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, 143 | }, 144 | { 145 | Name: "DataFragment invalid bytes", 146 | Bytes: []byte{0x08, 0x01}, 147 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 2 bytes are expected"), 148 | }, 149 | { 150 | Name: "FragSessionStatusReq", 151 | Command: Command{ 152 | CID: FragSessionStatusReq, 153 | Payload: &FragSessionStatusReqPayload{ 154 | FragStatusReqParam: FragSessionStatusReqPayloadFragStatusReqParam{ 155 | Participants: true, 156 | FragIndex: 3, 157 | }, 158 | }, 159 | }, 160 | Bytes: []byte{0x01, 0x07}, 161 | }, 162 | { 163 | Name: "FragSessionStatusReq invalid bytes", 164 | Bytes: []byte{0x01}, 165 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 1 byte is expected"), 166 | }, 167 | { 168 | Name: "FragSessionStatusAns", 169 | Uplink: true, 170 | Command: Command{ 171 | CID: FragSessionStatusAns, 172 | Payload: &FragSessionStatusAnsPayload{ 173 | ReceivedAndIndex: FragSessionStatusAnsPayloadReceivedAndIndex{ 174 | FragIndex: 3, 175 | NbFragReceived: 513, 176 | }, 177 | MissingFrag: 255, 178 | Status: FragSessionStatusAnsPayloadStatus{ 179 | NotEnoughMatrixMemory: true, 180 | }, 181 | }, 182 | }, 183 | Bytes: []byte{0x01, 0x01, 0xc2, 0xff, 0x01}, 184 | }, 185 | { 186 | Name: "FragSessionStatusAns invalid bytes", 187 | Uplink: true, 188 | Bytes: []byte{0x01, 0x01, 0xc2, 0xff}, 189 | ExpectedUnmarshalError: errors.New("lorawan/applayer/fragmentation: 4 bytes are expected"), 190 | }, 191 | } 192 | 193 | for _, tst := range tests { 194 | t.Run(tst.Name, func(t *testing.T) { 195 | assert := require.New(t) 196 | 197 | if tst.ExpectedMarshalError != nil { 198 | _, err := tst.Command.MarshalBinary() 199 | assert.Equal(tst.ExpectedMarshalError, err) 200 | } else if tst.ExpectedUnmarshalError != nil { 201 | var cmd Command 202 | err := cmd.UnmarshalBinary(tst.Uplink, tst.Bytes) 203 | assert.Equal(tst.ExpectedUnmarshalError, err) 204 | } else { 205 | cmds := Commands{tst.Command} 206 | b, err := cmds.MarshalBinary() 207 | assert.NoError(err) 208 | assert.Equal(tst.Bytes, b) 209 | 210 | cmds = Commands{} 211 | assert.NoError(cmds.UnmarshalBinary(tst.Uplink, tst.Bytes)) 212 | assert.Len(cmds, 1) 213 | assert.Equal(tst.Command, cmds[0]) 214 | } 215 | }) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /applayer/multicastsetup/cid_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=CID"; DO NOT EDIT. 2 | 3 | package multicastsetup 4 | 5 | import "strconv" 6 | 7 | const _CID_name = "PackageVersionReqMcGroupStatusReqMcGroupSetupReqMcGroupDeleteReqMcClassCSessionReqMcClassBSessionReq" 8 | 9 | var _CID_index = [...]uint8{0, 17, 33, 48, 64, 82, 100} 10 | 11 | func (i CID) String() string { 12 | if i >= CID(len(_CID_index)-1) { 13 | return "CID(" + strconv.FormatInt(int64(i), 10) + ")" 14 | } 15 | return _CID_name[_CID_index[i]:_CID_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /applayer/multicastsetup/keys.go: -------------------------------------------------------------------------------- 1 | package multicastsetup 2 | 3 | import ( 4 | "crypto/aes" 5 | "fmt" 6 | 7 | "github.com/brocaar/lorawan" 8 | ) 9 | 10 | // GetMcRootKeyForGenAppKey returns the McRootKey given a GenAppKey. 11 | // Note: The GenAppKey is only used for LoRaWAN 1.0.x devices. 12 | func GetMcRootKeyForGenAppKey(genAppKey lorawan.AES128Key) (lorawan.AES128Key, error) { 13 | return getKey(genAppKey, [16]byte{}) 14 | } 15 | 16 | // GetMcRootKeyForAppKey returns the McRootKey given an AppKey. 17 | // Note: The AppKey is only used for LoRaWAN 1.1.x devices. 18 | func GetMcRootKeyForAppKey(appKey lorawan.AES128Key) (lorawan.AES128Key, error) { 19 | return getKey(appKey, [16]byte{0x20}) 20 | } 21 | 22 | // GetMcKEKey returns the McKEKey given the McRootKey. 23 | func GetMcKEKey(mcRootKey lorawan.AES128Key) (lorawan.AES128Key, error) { 24 | return getKey(mcRootKey, [16]byte{}) 25 | } 26 | 27 | // GetMcAppSKey returns the McAppSKey given the McKey and McAddr. 28 | func GetMcAppSKey(mcKey lorawan.AES128Key, mcAddr lorawan.DevAddr) (lorawan.AES128Key, error) { 29 | b := [16]byte{0x01} 30 | 31 | mcAddrB, err := mcAddr.MarshalBinary() 32 | if err != nil { 33 | return lorawan.AES128Key{}, err 34 | } 35 | copy(b[1:5], mcAddrB) 36 | 37 | return getKey(mcKey, b) 38 | } 39 | 40 | // GetMcNetSKey returns the McNetSKey given the McKey and McAddr. 41 | func GetMcNetSKey(mcKey lorawan.AES128Key, mcAddr lorawan.DevAddr) (lorawan.AES128Key, error) { 42 | b := [16]byte{0x02} 43 | 44 | mcAddrB, err := mcAddr.MarshalBinary() 45 | if err != nil { 46 | return lorawan.AES128Key{}, err 47 | } 48 | copy(b[1:5], mcAddrB) 49 | 50 | return getKey(mcKey, b) 51 | } 52 | 53 | func getKey(key lorawan.AES128Key, b [16]byte) (lorawan.AES128Key, error) { 54 | var out lorawan.AES128Key 55 | 56 | block, err := aes.NewCipher(key[:]) 57 | if err != nil { 58 | return out, err 59 | } 60 | if block.BlockSize() != len(b) { 61 | return out, fmt.Errorf("block-size of %d bytes is expected", len(b)) 62 | } 63 | 64 | block.Encrypt(out[:], b[:]) 65 | return out, nil 66 | } 67 | -------------------------------------------------------------------------------- /applayer/multicastsetup/keys_test.go: -------------------------------------------------------------------------------- 1 | package multicastsetup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/brocaar/lorawan" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestKeys(t *testing.T) { 11 | mcAddr := lorawan.DevAddr{1, 2, 3, 4} 12 | mcKey := lorawan.AES128Key{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 13 | appKey := lorawan.AES128Key{2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 14 | genAppKey := lorawan.AES128Key{3, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 15 | mcRootKey := lorawan.AES128Key{4, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} 16 | 17 | t.Run("GetMcRootKeyForGenAppKey", func(t *testing.T) { 18 | assert := require.New(t) 19 | key, err := GetMcRootKeyForGenAppKey(genAppKey) 20 | assert.NoError(err) 21 | assert.Equal(lorawan.AES128Key{0x55, 0x34, 0x4e, 0x82, 0x57, 0xe, 0xae, 0xc8, 0xbf, 0x3, 0xb9, 0x99, 0x62, 0xd1, 0xf4, 0x45}, key) 22 | }) 23 | 24 | t.Run("GetMcRootKeyForAppKey", func(t *testing.T) { 25 | assert := require.New(t) 26 | key, err := GetMcRootKeyForAppKey(appKey) 27 | assert.NoError(err) 28 | assert.Equal(lorawan.AES128Key{0x26, 0x4f, 0xd8, 0x59, 0x58, 0x3f, 0xcc, 0x67, 0x2, 0x41, 0xac, 0x7, 0x1c, 0xc9, 0xf5, 0xbb}, key) 29 | }) 30 | 31 | t.Run("GetMcKEKey", func(t *testing.T) { 32 | assert := require.New(t) 33 | key, err := GetMcKEKey(mcRootKey) 34 | assert.NoError(err) 35 | assert.Equal(lorawan.AES128Key{0x90, 0x83, 0xbe, 0xbf, 0x70, 0x42, 0x57, 0x88, 0x31, 0x60, 0xdb, 0xfc, 0xde, 0x33, 0xad, 0x71}, key) 36 | }) 37 | 38 | t.Run("GetMcAppSKey", func(t *testing.T) { 39 | assert := require.New(t) 40 | key, err := GetMcAppSKey(mcKey, mcAddr) 41 | assert.NoError(err) 42 | assert.Equal(lorawan.AES128Key{0x95, 0xcb, 0x45, 0x18, 0xee, 0x37, 0x56, 0x6, 0x73, 0x5b, 0xba, 0xcb, 0xdc, 0xe8, 0x37, 0xfa}, key) 43 | }) 44 | 45 | t.Run("GetMcNetSKey", func(t *testing.T) { 46 | assert := require.New(t) 47 | key, err := GetMcNetSKey(mcKey, mcAddr) 48 | assert.NoError(err) 49 | assert.Equal(lorawan.AES128Key{0xc3, 0xf6, 0xb3, 0x88, 0xba, 0xd6, 0xc0, 0x0, 0xb2, 0x32, 0x91, 0xad, 0x52, 0xc1, 0x1c, 0x7b}, key) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /backend/backend_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | . "github.com/smartystreets/goconvey/convey" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestHEXBytes(t *testing.T) { 14 | Convey("Given a HEXBytes", t, func() { 15 | hb := HEXBytes{1, 2, 3, 4} 16 | 17 | Convey("Then String returns the expected string", func() { 18 | So(hb.String(), ShouldEqual, "01020304") 19 | }) 20 | 21 | Convey("Then MarshalText returns the expected output", func() { 22 | txt, err := hb.MarshalText() 23 | So(err, ShouldBeNil) 24 | So(string(txt), ShouldEqual, "01020304") 25 | }) 26 | }) 27 | 28 | Convey("Given an empty HEXBytes", t, func() { 29 | hb := HEXBytes{} 30 | 31 | Convey("Then UnmarshalText(\"01020304\") results in the expected HEXBytes", func() { 32 | So(hb.UnmarshalText([]byte("01020304")), ShouldBeNil) 33 | So(hb, ShouldResemble, HEXBytes{1, 2, 3, 4}) 34 | }) 35 | }) 36 | } 37 | 38 | func TestFrequency(t *testing.T) { 39 | Convey("Given a Frequency instance", t, func() { 40 | f := Frequency(868100000) 41 | 42 | Convey("Then MarshalJSON returns the expected value", func() { 43 | b, err := f.MarshalJSON() 44 | So(err, ShouldBeNil) 45 | So(string(b), ShouldEqual, "868.1") 46 | }) 47 | 48 | Convey("Then UnmarshalJSON unmarshals to the expected value", func() { 49 | So(f.UnmarshalJSON([]byte("868.2")), ShouldBeNil) 50 | So(f, ShouldEqual, Frequency(868200000)) 51 | }) 52 | }) 53 | } 54 | 55 | func TestPercentage(t *testing.T) { 56 | Convey("Given a Percentage instance", t, func() { 57 | p := Percentage(1) 58 | 59 | Convey("Then MarshalJSON returns the exepcted value", func() { 60 | b, err := p.MarshalJSON() 61 | So(err, ShouldBeNil) 62 | So(string(b), ShouldEqual, "0.01") 63 | }) 64 | 65 | Convey("Then UnmarshalJSON unmarshals to the expected value", func() { 66 | So(p.UnmarshalJSON([]byte("0.02")), ShouldBeNil) 67 | So(p, ShouldEqual, Percentage(2)) 68 | }) 69 | }) 70 | } 71 | 72 | func TestISO8601Time(t *testing.T) { 73 | Convey("Given an ISO8601Time instance", t, func() { 74 | ts := time.Date(2017, 12, 27, 17, 6, 35, 0, time.UTC) 75 | isoTS := ISO8601Time(ts) 76 | 77 | Convey("Then MarshalJSON returns the expected value", func() { 78 | b, err := json.Marshal(isoTS) 79 | So(err, ShouldBeNil) 80 | So(string(b), ShouldEqual, `"2017-12-27T17:06:35Z"`) 81 | }) 82 | 83 | Convey("Then UnmarshalJSON unmarshals to the expected value", func() { 84 | var ts2 time.Time 85 | So(json.Unmarshal([]byte(`"2017-12-27T17:06:35Z"`), &ts2), ShouldBeNil) 86 | So(ts2.Equal(ts), ShouldBeTrue) 87 | }) 88 | }) 89 | } 90 | 91 | func TestKeyEnvelope(t *testing.T) { 92 | key := lorawan.AES128Key{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8} 93 | kek := lorawan.AES128Key{8, 7, 6, 5, 4, 3, 2, 1, 8, 7, 6, 5, 4, 3, 2, 1} 94 | 95 | t.Run("No kek label", func(t *testing.T) { 96 | assert := require.New(t) 97 | 98 | ke, err := NewKeyEnvelope("", nil, key) 99 | assert.NoError(err) 100 | assert.Equal(&KeyEnvelope{ 101 | AESKey: HEXBytes(key[:]), 102 | }, ke) 103 | }) 104 | 105 | t.Run("With kek label", func(t *testing.T) { 106 | assert := require.New(t) 107 | 108 | ke, err := NewKeyEnvelope("test-kek", kek[:], key) 109 | assert.NoError(err) 110 | assert.Equal(&KeyEnvelope{ 111 | KEKLabel: "test-kek", 112 | AESKey: HEXBytes([]byte{0xe3, 0xd5, 0xa4, 0x7b, 0xa2, 0x5c, 0xbe, 0x6e, 0x5d, 0xa8, 0x20, 0x84, 0x6e, 0xc, 0xb6, 0xa8, 0x2b, 0x75, 0xc, 0x59, 0xd8, 0x48, 0xec, 0x7a}), 113 | }, ke) 114 | 115 | t.Run("Unwrap", func(t *testing.T) { 116 | assert := require.New(t) 117 | 118 | keyRet, err := ke.Unwrap(kek[:]) 119 | assert.NoError(err) 120 | assert.Equal(key, keyRet) 121 | }) 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /backend/joinserver/context.go: -------------------------------------------------------------------------------- 1 | package joinserver 2 | 3 | import ( 4 | "github.com/brocaar/lorawan" 5 | "github.com/brocaar/lorawan/backend" 6 | ) 7 | 8 | type context struct { 9 | joinReqPayload backend.JoinReqPayload 10 | joinAnsPayload backend.JoinAnsPayload 11 | rejoinReqPayload backend.RejoinReqPayload 12 | rejoinAnsPaylaod backend.RejoinAnsPayload 13 | joinType lorawan.JoinType 14 | phyPayload lorawan.PHYPayload 15 | deviceKeys DeviceKeys 16 | devNonce lorawan.DevNonce 17 | joinNonce lorawan.JoinNonce 18 | netID lorawan.NetID 19 | devEUI lorawan.EUI64 20 | joinEUI lorawan.EUI64 21 | fNwkSIntKey lorawan.AES128Key 22 | appSKey lorawan.AES128Key 23 | sNwkSIntKey lorawan.AES128Key 24 | nwkSEncKey lorawan.AES128Key 25 | nsKEKLabel string 26 | nsKEK []byte 27 | asKEKLabel string 28 | asKEK []byte 29 | } 30 | -------------------------------------------------------------------------------- /backend/joinserver/errors.go: -------------------------------------------------------------------------------- 1 | package joinserver 2 | 3 | import "errors" 4 | 5 | // Errors 6 | var ( 7 | ErrInvalidMIC = errors.New("invalid mic") 8 | ErrDevEUINotFound = errors.New("deveui does not exist") 9 | ) 10 | -------------------------------------------------------------------------------- /backend/joinserver/join_request.go: -------------------------------------------------------------------------------- 1 | package joinserver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/brocaar/lorawan" 7 | "github.com/pkg/errors" 8 | 9 | "github.com/brocaar/lorawan/backend" 10 | ) 11 | 12 | var joinTasks = []func(*context) error{ 13 | setJoinContext, 14 | validateMIC, 15 | setJoinNonce, 16 | setSessionKeys, 17 | createJoinAnsPayload, 18 | } 19 | 20 | func handleJoinRequestWrapper(joinReqPL backend.JoinReqPayload, dk DeviceKeys, asKEKLabel string, asKEK []byte, nsKEKLabel string, nsKEK []byte) backend.JoinAnsPayload { 21 | basePayload := backend.BasePayload{ 22 | ProtocolVersion: backend.ProtocolVersion1_0, 23 | SenderID: joinReqPL.ReceiverID, 24 | ReceiverID: joinReqPL.SenderID, 25 | TransactionID: joinReqPL.TransactionID, 26 | MessageType: backend.JoinAns, 27 | } 28 | 29 | jaPL, err := handleJoinRequest(joinReqPL, dk, asKEKLabel, asKEK, nsKEKLabel, nsKEK) 30 | if err != nil { 31 | var resCode backend.ResultCode 32 | 33 | switch errors.Cause(err) { 34 | case ErrInvalidMIC: 35 | resCode = backend.MICFailed 36 | default: 37 | resCode = backend.Other 38 | } 39 | 40 | jaPL = backend.JoinAnsPayload{ 41 | BasePayloadResult: backend.BasePayloadResult{ 42 | BasePayload: basePayload, 43 | Result: backend.Result{ 44 | ResultCode: resCode, 45 | Description: err.Error(), 46 | }, 47 | }, 48 | } 49 | } 50 | 51 | jaPL.BasePayload = basePayload 52 | return jaPL 53 | } 54 | 55 | func handleJoinRequest(joinReqPL backend.JoinReqPayload, dk DeviceKeys, asKEKLabel string, asKEK []byte, nsKEKLabel string, nsKEK []byte) (backend.JoinAnsPayload, error) { 56 | ctx := context{ 57 | joinReqPayload: joinReqPL, 58 | deviceKeys: dk, 59 | asKEKLabel: asKEKLabel, 60 | asKEK: asKEK, 61 | nsKEKLabel: nsKEKLabel, 62 | nsKEK: nsKEK, 63 | } 64 | 65 | for _, f := range joinTasks { 66 | if err := f(&ctx); err != nil { 67 | return ctx.joinAnsPayload, err 68 | } 69 | } 70 | 71 | return ctx.joinAnsPayload, nil 72 | } 73 | 74 | func setJoinContext(ctx *context) error { 75 | if err := ctx.phyPayload.UnmarshalBinary(ctx.joinReqPayload.PHYPayload[:]); err != nil { 76 | return errors.Wrap(err, "unmarshal phypayload error") 77 | } 78 | 79 | if err := ctx.netID.UnmarshalText([]byte(ctx.joinReqPayload.SenderID)); err != nil { 80 | return errors.Wrap(err, "unmarshal netid error") 81 | } 82 | 83 | if err := ctx.joinEUI.UnmarshalText([]byte(ctx.joinReqPayload.ReceiverID)); err != nil { 84 | return errors.Wrap(err, "unmarshal joineui error") 85 | } 86 | 87 | ctx.devEUI = ctx.joinReqPayload.DevEUI 88 | ctx.joinType = lorawan.JoinRequestType 89 | 90 | switch v := ctx.phyPayload.MACPayload.(type) { 91 | case *lorawan.JoinRequestPayload: 92 | ctx.devNonce = v.DevNonce 93 | default: 94 | return fmt.Errorf("expected *lorawan.JoinRequestPayload, got %T", ctx.phyPayload.MACPayload) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func validateMIC(ctx *context) error { 101 | ok, err := ctx.phyPayload.ValidateUplinkJoinMIC(ctx.deviceKeys.NwkKey) 102 | if err != nil { 103 | return errors.Wrap(err, "validate mic error") 104 | } 105 | if !ok { 106 | return ErrInvalidMIC 107 | } 108 | return nil 109 | } 110 | 111 | func setJoinNonce(ctx *context) error { 112 | if ctx.deviceKeys.JoinNonce > (1<<24)-1 { 113 | return errors.New("join-nonce overflow") 114 | } 115 | ctx.joinNonce = lorawan.JoinNonce(ctx.deviceKeys.JoinNonce) 116 | return nil 117 | } 118 | 119 | func setSessionKeys(ctx *context) error { 120 | var err error 121 | 122 | ctx.fNwkSIntKey, err = getFNwkSIntKey(ctx.joinReqPayload.DLSettings.OptNeg, ctx.deviceKeys.NwkKey, ctx.netID, ctx.joinEUI, ctx.joinNonce, ctx.devNonce) 123 | if err != nil { 124 | return errors.Wrap(err, "get FNwkSIntKey error") 125 | } 126 | 127 | if ctx.joinReqPayload.DLSettings.OptNeg { 128 | ctx.appSKey, err = getAppSKey(ctx.joinReqPayload.DLSettings.OptNeg, ctx.deviceKeys.AppKey, ctx.netID, ctx.joinEUI, ctx.joinNonce, ctx.devNonce) 129 | if err != nil { 130 | return errors.Wrap(err, "get AppSKey error") 131 | } 132 | } else { 133 | ctx.appSKey, err = getAppSKey(ctx.joinReqPayload.DLSettings.OptNeg, ctx.deviceKeys.NwkKey, ctx.netID, ctx.joinEUI, ctx.joinNonce, ctx.devNonce) 134 | if err != nil { 135 | return errors.Wrap(err, "get AppSKey error") 136 | } 137 | } 138 | 139 | ctx.sNwkSIntKey, err = getSNwkSIntKey(ctx.joinReqPayload.DLSettings.OptNeg, ctx.deviceKeys.NwkKey, ctx.netID, ctx.joinEUI, ctx.joinNonce, ctx.devNonce) 140 | if err != nil { 141 | return errors.Wrap(err, "get SNwkSIntKey error") 142 | } 143 | 144 | ctx.nwkSEncKey, err = getNwkSEncKey(ctx.joinReqPayload.DLSettings.OptNeg, ctx.deviceKeys.NwkKey, ctx.netID, ctx.joinEUI, ctx.joinNonce, ctx.devNonce) 145 | if err != nil { 146 | return errors.Wrap(err, "get NwkSEncKey error") 147 | } 148 | 149 | return nil 150 | } 151 | 152 | func createJoinAnsPayload(ctx *context) error { 153 | var cFList *lorawan.CFList 154 | if len(ctx.joinReqPayload.CFList[:]) != 0 { 155 | cFList = new(lorawan.CFList) 156 | if err := cFList.UnmarshalBinary(ctx.joinReqPayload.CFList[:]); err != nil { 157 | return errors.Wrap(err, "unmarshal cflist error") 158 | } 159 | } 160 | 161 | phy := lorawan.PHYPayload{ 162 | MHDR: lorawan.MHDR{ 163 | MType: lorawan.JoinAccept, 164 | Major: lorawan.LoRaWANR1, 165 | }, 166 | MACPayload: &lorawan.JoinAcceptPayload{ 167 | JoinNonce: ctx.joinNonce, 168 | HomeNetID: ctx.netID, 169 | DevAddr: ctx.joinReqPayload.DevAddr, 170 | DLSettings: ctx.joinReqPayload.DLSettings, 171 | RXDelay: uint8(ctx.joinReqPayload.RxDelay), 172 | CFList: cFList, 173 | }, 174 | } 175 | 176 | if ctx.joinReqPayload.DLSettings.OptNeg { 177 | jsIntKey, err := getJSIntKey(ctx.deviceKeys.NwkKey, ctx.devEUI) 178 | if err != nil { 179 | return err 180 | } 181 | if err := phy.SetDownlinkJoinMIC(ctx.joinType, ctx.joinEUI, ctx.devNonce, jsIntKey); err != nil { 182 | return err 183 | } 184 | } else { 185 | if err := phy.SetDownlinkJoinMIC(ctx.joinType, ctx.joinEUI, ctx.devNonce, ctx.deviceKeys.NwkKey); err != nil { 186 | return err 187 | } 188 | } 189 | 190 | if err := phy.EncryptJoinAcceptPayload(ctx.deviceKeys.NwkKey); err != nil { 191 | return err 192 | } 193 | 194 | b, err := phy.MarshalBinary() 195 | if err != nil { 196 | return err 197 | } 198 | 199 | ctx.joinAnsPayload = backend.JoinAnsPayload{ 200 | BasePayloadResult: backend.BasePayloadResult{ 201 | Result: backend.Result{ 202 | ResultCode: backend.Success, 203 | }, 204 | }, 205 | PHYPayload: backend.HEXBytes(b), 206 | // TODO add Lifetime? 207 | } 208 | 209 | ctx.joinAnsPayload.AppSKey, err = backend.NewKeyEnvelope(ctx.asKEKLabel, ctx.asKEK, ctx.appSKey) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | if ctx.joinReqPayload.DLSettings.OptNeg { 215 | // LoRaWAN 1.1+ 216 | ctx.joinAnsPayload.FNwkSIntKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.fNwkSIntKey) 217 | if err != nil { 218 | return err 219 | } 220 | ctx.joinAnsPayload.SNwkSIntKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.sNwkSIntKey) 221 | if err != nil { 222 | return err 223 | } 224 | ctx.joinAnsPayload.NwkSEncKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.nwkSEncKey) 225 | if err != nil { 226 | return err 227 | } 228 | } else { 229 | // LoRaWAN 1.0.x 230 | ctx.joinAnsPayload.NwkSKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.fNwkSIntKey) 231 | if err != nil { 232 | return err 233 | } 234 | } 235 | 236 | return nil 237 | } 238 | -------------------------------------------------------------------------------- /backend/joinserver/rejoin_request.go: -------------------------------------------------------------------------------- 1 | package joinserver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/brocaar/lorawan" 9 | "github.com/brocaar/lorawan/backend" 10 | ) 11 | 12 | var rejoinTasks = []func(*context) error{ 13 | setRejoinContext, 14 | setJoinNonce, 15 | setSessionKeys, 16 | createRejoinAnsPayload, 17 | } 18 | 19 | func handleRejoinRequestWrapper(rejoinReqPL backend.RejoinReqPayload, dk DeviceKeys, asKEKLabel string, asKEK []byte, nsKEKLabel string, nsKEK []byte) backend.RejoinAnsPayload { 20 | basePayload := backend.BasePayload{ 21 | ProtocolVersion: backend.ProtocolVersion1_0, 22 | SenderID: rejoinReqPL.ReceiverID, 23 | ReceiverID: rejoinReqPL.SenderID, 24 | TransactionID: rejoinReqPL.TransactionID, 25 | MessageType: backend.RejoinAns, 26 | } 27 | 28 | rjaPL, err := handleRejoinRequest(rejoinReqPL, dk, asKEKLabel, asKEK, nsKEKLabel, nsKEK) 29 | if err != nil { 30 | var resCode backend.ResultCode 31 | 32 | switch errors.Cause(err) { 33 | case ErrInvalidMIC: 34 | resCode = backend.MICFailed 35 | default: 36 | resCode = backend.Other 37 | } 38 | 39 | rjaPL = backend.RejoinAnsPayload{ 40 | BasePayloadResult: backend.BasePayloadResult{ 41 | BasePayload: basePayload, 42 | Result: backend.Result{ 43 | ResultCode: resCode, 44 | Description: err.Error(), 45 | }, 46 | }, 47 | } 48 | } 49 | 50 | rjaPL.BasePayload = basePayload 51 | return rjaPL 52 | } 53 | 54 | func handleRejoinRequest(rejoinReqPL backend.RejoinReqPayload, dk DeviceKeys, asKEKLabel string, asKEK []byte, nsKEKLabel string, nsKEK []byte) (backend.RejoinAnsPayload, error) { 55 | ctx := context{ 56 | rejoinReqPayload: rejoinReqPL, 57 | deviceKeys: dk, 58 | asKEKLabel: asKEKLabel, 59 | asKEK: asKEK, 60 | nsKEKLabel: nsKEKLabel, 61 | nsKEK: nsKEK, 62 | } 63 | 64 | for _, f := range rejoinTasks { 65 | if err := f(&ctx); err != nil { 66 | return ctx.rejoinAnsPaylaod, err 67 | } 68 | } 69 | 70 | return ctx.rejoinAnsPaylaod, nil 71 | } 72 | 73 | func setRejoinContext(ctx *context) error { 74 | if err := ctx.phyPayload.UnmarshalBinary(ctx.rejoinReqPayload.PHYPayload[:]); err != nil { 75 | return errors.Wrap(err, "unmarshal phypayload error") 76 | } 77 | 78 | if err := ctx.netID.UnmarshalText([]byte(ctx.rejoinReqPayload.SenderID)); err != nil { 79 | return errors.Wrap(err, "unmarshal netid error") 80 | } 81 | 82 | if err := ctx.joinEUI.UnmarshalText([]byte(ctx.rejoinReqPayload.ReceiverID)); err != nil { 83 | return errors.Wrap(err, "unmarshal joineui error") 84 | } 85 | 86 | switch v := ctx.phyPayload.MACPayload.(type) { 87 | case *lorawan.RejoinRequestType02Payload: 88 | ctx.joinType = v.RejoinType 89 | ctx.devNonce = lorawan.DevNonce(v.RJCount0) 90 | case *lorawan.RejoinRequestType1Payload: 91 | ctx.joinType = v.RejoinType 92 | ctx.devNonce = lorawan.DevNonce(v.RJCount1) 93 | default: 94 | return fmt.Errorf("expected rejoin payload, got %T", ctx.phyPayload.MACPayload) 95 | } 96 | 97 | ctx.devEUI = ctx.rejoinReqPayload.DevEUI 98 | 99 | return nil 100 | } 101 | 102 | func createRejoinAnsPayload(ctx *context) error { 103 | var cFList *lorawan.CFList 104 | if len(ctx.rejoinReqPayload.CFList[:]) != 0 { 105 | cFList = new(lorawan.CFList) 106 | if err := cFList.UnmarshalBinary(ctx.rejoinReqPayload.CFList[:]); err != nil { 107 | return errors.Wrap(err, "unmarshal cflist error") 108 | } 109 | } 110 | 111 | phy := lorawan.PHYPayload{ 112 | MHDR: lorawan.MHDR{ 113 | MType: lorawan.JoinAccept, 114 | Major: lorawan.LoRaWANR1, 115 | }, 116 | MACPayload: &lorawan.JoinAcceptPayload{ 117 | JoinNonce: ctx.joinNonce, 118 | HomeNetID: ctx.netID, 119 | DevAddr: ctx.rejoinReqPayload.DevAddr, 120 | DLSettings: ctx.rejoinReqPayload.DLSettings, 121 | RXDelay: uint8(ctx.rejoinReqPayload.RxDelay), 122 | CFList: cFList, 123 | }, 124 | } 125 | 126 | jsIntKey, err := getJSIntKey(ctx.deviceKeys.NwkKey, ctx.devEUI) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | jsEncKey, err := getJSEncKey(ctx.deviceKeys.NwkKey, ctx.devEUI) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | if err := phy.SetDownlinkJoinMIC(ctx.joinType, ctx.joinEUI, ctx.devNonce, jsIntKey); err != nil { 137 | return err 138 | } 139 | 140 | if err := phy.EncryptJoinAcceptPayload(jsEncKey); err != nil { 141 | return err 142 | } 143 | 144 | b, err := phy.MarshalBinary() 145 | if err != nil { 146 | return err 147 | } 148 | 149 | // as the rejoin-request is only implemented for LoRaWAN1.1+ there is no 150 | // need to check the OptNeg flag 151 | ctx.rejoinAnsPaylaod = backend.RejoinAnsPayload{ 152 | BasePayloadResult: backend.BasePayloadResult{ 153 | Result: backend.Result{ 154 | ResultCode: backend.Success, 155 | }, 156 | }, 157 | PHYPayload: backend.HEXBytes(b), 158 | // TODO: add Lifetime? 159 | } 160 | 161 | ctx.rejoinAnsPaylaod.AppSKey, err = backend.NewKeyEnvelope(ctx.asKEKLabel, ctx.asKEK, ctx.appSKey) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | ctx.rejoinAnsPaylaod.FNwkSIntKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.fNwkSIntKey) 167 | if err != nil { 168 | return err 169 | } 170 | ctx.rejoinAnsPaylaod.SNwkSIntKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.sNwkSIntKey) 171 | if err != nil { 172 | return err 173 | } 174 | ctx.rejoinAnsPaylaod.NwkSEncKey, err = backend.NewKeyEnvelope(ctx.nsKEKLabel, ctx.nsKEK, ctx.nwkSEncKey) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /backend/joinserver/session_keys.go: -------------------------------------------------------------------------------- 1 | package joinserver 2 | 3 | import ( 4 | "crypto/aes" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/brocaar/lorawan" 10 | ) 11 | 12 | // getFNwkSIntKey returns the FNwkSIntKey. 13 | // For LoRaWAN 1.0: SNwkSIntKey = NwkSEncKey = FNwkSIntKey = NwkSKey 14 | func getFNwkSIntKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 15 | return getSKey(optNeg, 0x01, nwkKey, netID, joinEUI, joinNonce, devNonce) 16 | } 17 | 18 | // getAppSKey returns appSKey. 19 | func getAppSKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 20 | return getSKey(optNeg, 0x02, nwkKey, netID, joinEUI, joinNonce, devNonce) 21 | } 22 | 23 | // getSNwkSIntKey returns the NwkSIntKey. 24 | func getSNwkSIntKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 25 | return getSKey(optNeg, 0x03, nwkKey, netID, joinEUI, joinNonce, devNonce) 26 | } 27 | 28 | // getNwkSEncKey returns the NwkSEncKey. 29 | func getNwkSEncKey(optNeg bool, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 30 | return getSKey(optNeg, 0x04, nwkKey, netID, joinEUI, joinNonce, devNonce) 31 | } 32 | 33 | // getJSIntKey returns the JSIntKey. 34 | func getJSIntKey(nwkKey lorawan.AES128Key, devEUI lorawan.EUI64) (lorawan.AES128Key, error) { 35 | return getJSKey(0x06, devEUI, nwkKey) 36 | } 37 | 38 | // getJSEncKey returns the JSEncKey. 39 | func getJSEncKey(nwkKey lorawan.AES128Key, devEUI lorawan.EUI64) (lorawan.AES128Key, error) { 40 | return getJSKey(0x05, devEUI, nwkKey) 41 | } 42 | 43 | func getSKey(optNeg bool, typ byte, nwkKey lorawan.AES128Key, netID lorawan.NetID, joinEUI lorawan.EUI64, joinNonce lorawan.JoinNonce, devNonce lorawan.DevNonce) (lorawan.AES128Key, error) { 44 | var key lorawan.AES128Key 45 | b := make([]byte, 16) 46 | b[0] = typ 47 | 48 | netIDB, err := netID.MarshalBinary() 49 | if err != nil { 50 | return key, errors.Wrap(err, "marshal binary error") 51 | } 52 | 53 | joinEUIB, err := joinEUI.MarshalBinary() 54 | if err != nil { 55 | return key, errors.Wrap(err, "marshal binary error") 56 | } 57 | 58 | joinNonceB, err := joinNonce.MarshalBinary() 59 | if err != nil { 60 | return key, errors.Wrap(err, "marshal binary error") 61 | } 62 | 63 | devNonceB, err := devNonce.MarshalBinary() 64 | if err != nil { 65 | return key, errors.Wrap(err, "marshal binary error") 66 | } 67 | 68 | if optNeg { 69 | copy(b[1:4], joinNonceB) 70 | copy(b[4:12], joinEUIB) 71 | copy(b[12:14], devNonceB) 72 | } else { 73 | copy(b[1:4], joinNonceB) 74 | copy(b[4:7], netIDB) 75 | copy(b[7:9], devNonceB) 76 | } 77 | 78 | block, err := aes.NewCipher(nwkKey[:]) 79 | if err != nil { 80 | return key, err 81 | } 82 | if block.BlockSize() != len(b) { 83 | return key, fmt.Errorf("block-size of %d bytes is expected", len(b)) 84 | } 85 | block.Encrypt(key[:], b) 86 | 87 | return key, nil 88 | } 89 | 90 | func getJSKey(typ byte, devEUI lorawan.EUI64, nwkKey lorawan.AES128Key) (lorawan.AES128Key, error) { 91 | var key lorawan.AES128Key 92 | b := make([]byte, 16) 93 | 94 | b[0] = typ 95 | 96 | devB, err := devEUI.MarshalBinary() 97 | if err != nil { 98 | return key, err 99 | } 100 | copy(b[1:9], devB[:]) 101 | 102 | block, err := aes.NewCipher(nwkKey[:]) 103 | if err != nil { 104 | return key, err 105 | } 106 | if block.BlockSize() != len(b) { 107 | return key, fmt.Errorf("block-size of %d bytes is expected", len(b)) 108 | } 109 | block.Encrypt(key[:], b) 110 | return key, nil 111 | } 112 | -------------------------------------------------------------------------------- /band/band_as923_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/brocaar/lorawan" 10 | ) 11 | 12 | func TestAS923_1_Band(t *testing.T) { 13 | t.Run("400ms dwell-time", func(t *testing.T) { 14 | assert := require.New(t) 15 | 16 | band, err := GetConfig(AS923, true, lorawan.DwellTime400ms) 17 | assert.NoError(err) 18 | 19 | assert.Equal("AS923", band.Name()) 20 | 21 | t.Run("GetDefaults", func(t *testing.T) { 22 | assert := require.New(t) 23 | assert.Equal(Defaults{ 24 | RX2Frequency: 923200000, 25 | RX2DataRate: 2, 26 | ReceiveDelay1: time.Second, 27 | ReceiveDelay2: time.Second * 2, 28 | JoinAcceptDelay1: time.Second * 5, 29 | JoinAcceptDelay2: time.Second * 6, 30 | }, band.GetDefaults()) 31 | }) 32 | 33 | t.Run("GetDownlinkTXPower", func(t *testing.T) { 34 | assert := require.New(t) 35 | assert.Equal(14, band.GetDownlinkTXPower(0)) 36 | }) 37 | 38 | t.Run("GetPingSlotFrequency", func(t *testing.T) { 39 | assert := require.New(t) 40 | freq, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 41 | assert.NoError(err) 42 | assert.EqualValues(923400000, freq) 43 | }) 44 | 45 | t.Run("GetRX1ChannelIndexForUplinkChannelIndex", func(t *testing.T) { 46 | assert := require.New(t) 47 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(2) 48 | assert.NoError(err) 49 | assert.Equal(2, c) 50 | }) 51 | 52 | t.Run("RX1FrequencyForUplinkFrequency", func(t *testing.T) { 53 | assert := require.New(t) 54 | f, err := band.GetRX1FrequencyForUplinkFrequency(923200000) 55 | assert.NoError(err) 56 | assert.EqualValues(923200000, f) 57 | }) 58 | 59 | t.Run("GetRX1DataRateIndex", func(t *testing.T) { 60 | assert := require.New(t) 61 | tests := []struct { 62 | UplinkDR int 63 | RX1DROffset int 64 | ExpectedDR int 65 | }{ 66 | {5, 0, 5}, 67 | {5, 1, 4}, 68 | {5, 2, 3}, 69 | {5, 3, 2}, 70 | {5, 4, 2}, 71 | {5, 5, 2}, 72 | {5, 6, 5}, 73 | {5, 7, 5}, 74 | {2, 6, 3}, 75 | {2, 7, 4}, 76 | } 77 | 78 | for _, tst := range tests { 79 | dr, err := band.GetRX1DataRateIndex(tst.UplinkDR, tst.RX1DROffset) 80 | assert.NoError(err) 81 | assert.Equal(tst.ExpectedDR, dr) 82 | } 83 | }) 84 | }) 85 | 86 | t.Run("No dwell-time", func(t *testing.T) { 87 | assert := require.New(t) 88 | 89 | band, err := GetConfig(AS_923, true, lorawan.DwellTimeNoLimit) 90 | assert.NoError(err) 91 | 92 | t.Run("GetRX1DataRateIndex", func(t *testing.T) { 93 | assert := require.New(t) 94 | 95 | tests := []struct { 96 | UplinkDR int 97 | RX1DROffset int 98 | ExpectedDR int 99 | }{ 100 | {5, 0, 5}, 101 | {5, 1, 4}, 102 | {5, 2, 3}, 103 | {5, 3, 2}, 104 | {5, 4, 1}, 105 | {5, 5, 0}, 106 | {5, 6, 5}, 107 | {5, 7, 5}, 108 | {2, 6, 3}, 109 | {2, 7, 4}, 110 | } 111 | 112 | for _, tst := range tests { 113 | dr, err := band.GetRX1DataRateIndex(tst.UplinkDR, tst.RX1DROffset) 114 | assert.NoError(err) 115 | assert.Equal(tst.ExpectedDR, dr) 116 | } 117 | }) 118 | }) 119 | } 120 | 121 | func TestAS923_2_Band(t *testing.T) { 122 | assert := require.New(t) 123 | band, err := GetConfig(AS923_2, true, lorawan.DwellTimeNoLimit) 124 | assert.NoError(err) 125 | 126 | assert.Equal("AS923-2", band.Name()) 127 | 128 | assert.EqualValues(923200000-1800000, band.GetDefaults().RX2Frequency) 129 | freq, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 130 | assert.NoError(err) 131 | assert.EqualValues(923400000-1800000, freq) 132 | 133 | bandd := band.(*as923Band) 134 | 135 | assert.EqualValues(923200000-1800000, bandd.uplinkChannels[0].Frequency) 136 | assert.EqualValues(923200000-1800000, bandd.downlinkChannels[0].Frequency) 137 | assert.EqualValues(923400000-1800000, bandd.uplinkChannels[1].Frequency) 138 | assert.EqualValues(923400000-1800000, bandd.downlinkChannels[1].Frequency) 139 | } 140 | 141 | func TestAS923_3_Band(t *testing.T) { 142 | assert := require.New(t) 143 | band, err := GetConfig(AS923_3, true, lorawan.DwellTimeNoLimit) 144 | assert.NoError(err) 145 | 146 | assert.Equal("AS923-3", band.Name()) 147 | 148 | assert.EqualValues(923200000-6600000, band.GetDefaults().RX2Frequency) 149 | freq, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 150 | assert.NoError(err) 151 | assert.EqualValues(923400000-6600000, freq) 152 | 153 | bandd := band.(*as923Band) 154 | 155 | assert.EqualValues(923200000-6600000, bandd.uplinkChannels[0].Frequency) 156 | assert.EqualValues(923200000-6600000, bandd.downlinkChannels[0].Frequency) 157 | assert.EqualValues(923400000-6600000, bandd.uplinkChannels[1].Frequency) 158 | assert.EqualValues(923400000-6600000, bandd.downlinkChannels[1].Frequency) 159 | } 160 | -------------------------------------------------------------------------------- /band/band_au915_928_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestAU915Band(t *testing.T) { 13 | Convey("Given the AU 915-928 band is selected", t, func() { 14 | band, err := GetConfig(AU_915_928, true, lorawan.DwellTimeNoLimit) 15 | So(err, ShouldBeNil) 16 | 17 | Convey("Then GetDefaults returns the expected value", func() { 18 | So(band.GetDefaults(), ShouldResemble, Defaults{ 19 | RX2Frequency: 923300000, 20 | RX2DataRate: 8, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | }) 26 | }) 27 | 28 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 29 | So(band.GetDownlinkTXPower(0), ShouldEqual, 27) 30 | }) 31 | 32 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 33 | tests := []struct { 34 | DevAddr lorawan.DevAddr 35 | BeaconTime string 36 | ExpectedFrequency int 37 | }{ 38 | { 39 | DevAddr: lorawan.DevAddr{3, 20, 207, 54}, 40 | BeaconTime: "334382h51m44s", 41 | ExpectedFrequency: 925700000, 42 | }, 43 | } 44 | 45 | for _, test := range tests { 46 | bt, err := time.ParseDuration(test.BeaconTime) 47 | So(err, ShouldBeNil) 48 | freq, err := band.GetPingSlotFrequency(test.DevAddr, bt) 49 | So(err, ShouldBeNil) 50 | So(freq, ShouldEqual, test.ExpectedFrequency) 51 | } 52 | }) 53 | 54 | Convey("When testing the uplink channels", func() { 55 | testTable := []struct { 56 | Channel int 57 | Frequency uint32 58 | MinDR int 59 | MaxDR int 60 | }{ 61 | {Channel: 0, Frequency: 915200000, MinDR: 0, MaxDR: 5}, 62 | {Channel: 63, Frequency: 927800000, MinDR: 0, MaxDR: 5}, 63 | {Channel: 64, Frequency: 915900000, MinDR: 6, MaxDR: 7}, 64 | {Channel: 71, Frequency: 927100000, MinDR: 6, MaxDR: 7}, 65 | } 66 | 67 | for _, test := range testTable { 68 | Convey(fmt.Sprintf("Then channel %d must have frequency %d and min / max data-rates %d/%d", test.Channel, test.Frequency, test.MinDR, test.MaxDR), func() { 69 | c, err := band.GetUplinkChannel(test.Channel) 70 | So(err, ShouldBeNil) 71 | 72 | So(c.Frequency, ShouldEqual, test.Frequency) 73 | So(c.MinDR, ShouldEqual, test.MinDR) 74 | So(c.MaxDR, ShouldEqual, test.MaxDR) 75 | }) 76 | } 77 | }) 78 | 79 | Convey("When testing the downlink channels", func() { 80 | testTable := []struct { 81 | Frequency uint32 82 | DataRate int 83 | Channel int 84 | ExpFrequency uint32 85 | }{ 86 | {Frequency: 915900000, DataRate: 4, Channel: 64, ExpFrequency: 923300000}, 87 | {Frequency: 915200000, DataRate: 3, Channel: 0, ExpFrequency: 923300000}, 88 | } 89 | 90 | for _, test := range testTable { 91 | Convey(fmt.Sprintf("Then frequency: %d must return frequency: %d", test.Frequency, test.ExpFrequency), func() { 92 | txChan, err := band.GetUplinkChannelIndex(test.Frequency, true) 93 | So(err, ShouldBeNil) 94 | So(txChan, ShouldEqual, test.Channel) 95 | 96 | freq, err := band.GetRX1FrequencyForUplinkFrequency(test.Frequency) 97 | So(err, ShouldBeNil) 98 | So(freq, ShouldEqual, test.ExpFrequency) 99 | }) 100 | } 101 | }) 102 | 103 | Convey("Then GetDataRateIndex returns the expected data-rate index", func() { 104 | tests := []struct { 105 | DataRate DataRate 106 | Uplink bool 107 | ExpectedDR int 108 | }{ 109 | { 110 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125}, 111 | Uplink: true, 112 | ExpectedDR: 0, 113 | }, 114 | { 115 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 500}, 116 | Uplink: false, 117 | ExpectedDR: 8, 118 | }, 119 | { 120 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 500}, 121 | Uplink: true, 122 | ExpectedDR: 6, 123 | }, 124 | { 125 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 500}, 126 | Uplink: false, 127 | ExpectedDR: 12, 128 | }, 129 | } 130 | 131 | for _, t := range tests { 132 | dr, err := band.GetDataRateIndex(t.Uplink, t.DataRate) 133 | So(err, ShouldBeNil) 134 | So(dr, ShouldEqual, t.ExpectedDR) 135 | } 136 | }) 137 | 138 | Convey("When testing LinkADRReqPayload functions", func() { 139 | var filteredChans []int 140 | for i := 8; i < 72; i++ { 141 | filteredChans = append(filteredChans, i) 142 | } 143 | 144 | tests := []struct { 145 | Name string 146 | NodeChannels []int 147 | DisableChannels []int 148 | EnableChannels []int 149 | ExpectedUplinkChannels []int 150 | ExpectedLinkADRReqPayloads []lorawan.LinkADRReqPayload 151 | }{ 152 | { 153 | Name: "all channels active", 154 | NodeChannels: band.GetUplinkChannelIndices(), 155 | ExpectedUplinkChannels: band.GetUplinkChannelIndices(), 156 | }, 157 | { 158 | Name: "only activate channel 0 - 7", 159 | NodeChannels: band.GetEnabledUplinkChannelIndices(), 160 | DisableChannels: band.GetEnabledUplinkChannelIndices(), 161 | EnableChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 162 | ExpectedUplinkChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 163 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 164 | { 165 | Redundancy: lorawan.Redundancy{ChMaskCntl: 7}, 166 | }, 167 | { 168 | ChMask: lorawan.ChMask{true, true, true, true, true, true, true, true}, 169 | Redundancy: lorawan.Redundancy{ChMaskCntl: 0}, 170 | }, 171 | }, 172 | }, 173 | { 174 | Name: "only activate channel 8 - 23", 175 | NodeChannels: band.GetEnabledUplinkChannelIndices(), 176 | DisableChannels: band.GetEnabledUplinkChannelIndices(), 177 | EnableChannels: []int{8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, 178 | ExpectedUplinkChannels: []int{8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, 179 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 180 | { 181 | Redundancy: lorawan.Redundancy{ChMaskCntl: 7}, 182 | }, 183 | { 184 | ChMask: lorawan.ChMask{false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true}, 185 | Redundancy: lorawan.Redundancy{ChMaskCntl: 0}, 186 | }, 187 | { 188 | ChMask: lorawan.ChMask{true, true, true, true, true, true, true, true}, 189 | Redundancy: lorawan.Redundancy{ChMaskCntl: 1}, 190 | }, 191 | }, 192 | }, 193 | { 194 | Name: "only activate channel 64 - 71", 195 | NodeChannels: band.GetEnabledUplinkChannelIndices(), 196 | DisableChannels: band.GetEnabledUplinkChannelIndices(), 197 | EnableChannels: []int{64, 65, 66, 67, 68, 69, 70, 71}, 198 | ExpectedUplinkChannels: []int{64, 65, 66, 67, 68, 69, 70, 71}, 199 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 200 | { 201 | ChMask: lorawan.ChMask{true, true, true, true, true, true, true, true}, 202 | Redundancy: lorawan.Redundancy{ChMaskCntl: 7}, 203 | }, 204 | }, 205 | }, 206 | { 207 | Name: "only disable channel 0 - 7", 208 | NodeChannels: band.GetEnabledUplinkChannelIndices(), 209 | DisableChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 210 | ExpectedUplinkChannels: filteredChans, 211 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 212 | { 213 | ChMask: lorawan.ChMask{false, false, false, false, false, false, false, false, true, true, true, true, true, true, true, true}, 214 | Redundancy: lorawan.Redundancy{ChMaskCntl: 0}, 215 | }, 216 | }, 217 | }, 218 | } 219 | 220 | for i, test := range tests { 221 | Convey(fmt.Sprintf("testing %s [%d]", test.Name, i), func() { 222 | for _, c := range test.DisableChannels { 223 | So(band.DisableUplinkChannelIndex(c), ShouldBeNil) 224 | } 225 | for _, c := range test.EnableChannels { 226 | So(band.EnableUplinkChannelIndex(c), ShouldBeNil) 227 | } 228 | pls := band.GetLinkADRReqPayloadsForEnabledUplinkChannelIndices(test.NodeChannels) 229 | So(pls, ShouldResemble, test.ExpectedLinkADRReqPayloads) 230 | 231 | chans, err := band.GetEnabledUplinkChannelIndicesForLinkADRReqPayloads(test.NodeChannels, pls) 232 | So(err, ShouldBeNil) 233 | So(chans, ShouldResemble, test.ExpectedUplinkChannels) 234 | }) 235 | } 236 | }) 237 | }) 238 | } 239 | -------------------------------------------------------------------------------- /band/band_cn470_510.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "encoding/binary" 5 | "time" 6 | 7 | "github.com/brocaar/lorawan" 8 | ) 9 | 10 | type cn470Band struct { 11 | band 12 | } 13 | 14 | func (b *cn470Band) Name() string { 15 | return "CN470" 16 | } 17 | 18 | func (b *cn470Band) GetDefaults() Defaults { 19 | return Defaults{ 20 | RX2Frequency: 505300000, 21 | RX2DataRate: 0, 22 | ReceiveDelay1: time.Second, 23 | ReceiveDelay2: time.Second * 2, 24 | JoinAcceptDelay1: time.Second * 5, 25 | JoinAcceptDelay2: time.Second * 6, 26 | } 27 | } 28 | 29 | func (b *cn470Band) GetDownlinkTXPower(freq uint32) int { 30 | return 14 31 | } 32 | 33 | func (b *cn470Band) GetDefaultMaxUplinkEIRP() float32 { 34 | return 19.15 35 | } 36 | 37 | func (b *cn470Band) GetPingSlotFrequency(devAddr lorawan.DevAddr, beaconTime time.Duration) (uint32, error) { 38 | downlinkChannel := (int(binary.BigEndian.Uint32(devAddr[:])) + int(beaconTime/(128*time.Second))) % 8 39 | return []uint32{ 40 | 508300000, 41 | 508500000, 42 | 508700000, 43 | 508900000, 44 | 509100000, 45 | 509300000, 46 | 509500000, 47 | 509700000, 48 | }[downlinkChannel], nil 49 | } 50 | 51 | func (b *cn470Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 52 | return uplinkChannel % 48, nil 53 | } 54 | 55 | func (b *cn470Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 56 | uplinkChan, err := b.GetUplinkChannelIndex(uplinkFrequency, true) 57 | if err != nil { 58 | return 0, err 59 | } 60 | 61 | rx1Chan, err := b.GetRX1ChannelIndexForUplinkChannelIndex(uplinkChan) 62 | if err != nil { 63 | return 0, err 64 | } 65 | 66 | return b.downlinkChannels[rx1Chan].Frequency, nil 67 | } 68 | 69 | func (b *cn470Band) ImplementsTXParamSetup(protocolVersion string) bool { 70 | return false 71 | } 72 | 73 | func newCN470Band(repeaterCompatible bool) (Band, error) { 74 | b := cn470Band{ 75 | band: band{ 76 | dataRates: map[int]DataRate{ 77 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 78 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 79 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 80 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 81 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 82 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 83 | 6: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 500, uplink: true, downlink: true}, 84 | 7: {Modulation: FSKModulation, BitRate: 50000, uplink: true, downlink: true}, 85 | }, 86 | rx1DataRateTable: map[int][]int{ 87 | 0: {0, 0, 0, 0, 0, 0}, 88 | 1: {1, 0, 0, 0, 0, 0}, 89 | 2: {2, 1, 0, 0, 0, 0}, 90 | 3: {3, 2, 1, 0, 0, 0}, 91 | 4: {4, 3, 2, 1, 0, 0}, 92 | 5: {5, 4, 3, 2, 1, 0}, 93 | }, 94 | txPowerOffsets: []int{ 95 | 0, // 0 96 | -2, // 1 97 | -4, // 2 98 | -6, // 3 99 | -8, // 4 100 | -10, // 5 101 | -12, // 6 102 | -14, // 7 103 | }, 104 | uplinkChannels: make([]Channel, 96), 105 | downlinkChannels: make([]Channel, 48), 106 | }, 107 | } 108 | 109 | if repeaterCompatible { 110 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 111 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 112 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 113 | 0: {M: 59, N: 51}, 114 | 1: {M: 59, N: 51}, 115 | 2: {M: 59, N: 51}, 116 | 3: {M: 123, N: 115}, 117 | 4: {M: 230, N: 222}, 118 | 5: {M: 230, N: 222}, 119 | }, 120 | }, 121 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 122 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, 1.0.2B 123 | 0: {M: 59, N: 51}, 124 | 1: {M: 59, N: 51}, 125 | 2: {M: 59, N: 51}, 126 | 3: {M: 123, N: 115}, 127 | 4: {M: 230, N: 222}, 128 | 5: {M: 230, N: 222}, 129 | }, 130 | }, 131 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 132 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 133 | 0: {M: 59, N: 51}, 134 | 1: {M: 59, N: 51}, 135 | 2: {M: 59, N: 51}, 136 | 3: {M: 123, N: 115}, 137 | 4: {M: 230, N: 222}, 138 | 5: {M: 230, N: 222}, 139 | }, 140 | }, 141 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 142 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 143 | 0: {M: 59, N: 51}, 144 | 1: {M: 59, N: 51}, 145 | 2: {M: 59, N: 51}, 146 | 3: {M: 123, N: 115}, 147 | 4: {M: 230, N: 222}, 148 | 5: {M: 230, N: 222}, 149 | }, 150 | }, 151 | latest: map[string]map[int]MaxPayloadSize{ 152 | RegParamRevRP002_1_0_0: map[int]MaxPayloadSize{ // RP002-1.0.0 153 | 0: {M: 59, N: 51}, 154 | 1: {M: 59, N: 51}, 155 | 2: {M: 59, N: 51}, 156 | 3: {M: 123, N: 115}, 157 | 4: {M: 230, N: 222}, 158 | 5: {M: 230, N: 222}, 159 | }, 160 | RegParamRevRP002_1_0_1: map[int]MaxPayloadSize{ 161 | 0: {M: 0, N: 0}, 162 | 1: {M: 31, N: 23}, 163 | 2: {M: 94, N: 86}, 164 | 3: {M: 172, N: 164}, 165 | 4: {M: 230, N: 222}, 166 | 5: {M: 230, N: 222}, 167 | 6: {M: 230, N: 222}, 168 | 7: {M: 230, N: 222}, 169 | }, 170 | latest: map[int]MaxPayloadSize{ // RP002-1.0.2, RP002-1.0.3 171 | 0: {M: 0, N: 0}, 172 | 1: {M: 31, N: 23}, 173 | 2: {M: 94, N: 86}, 174 | 3: {M: 192, N: 184}, 175 | 4: {M: 230, N: 222}, 176 | 5: {M: 230, N: 222}, 177 | 6: {M: 230, N: 222}, 178 | 7: {M: 230, N: 222}, 179 | }, 180 | }, 181 | } 182 | } else { 183 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 184 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 185 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 186 | 0: {M: 59, N: 51}, 187 | 1: {M: 59, N: 51}, 188 | 2: {M: 59, N: 51}, 189 | 3: {M: 123, N: 115}, 190 | 4: {M: 230, N: 222}, 191 | 5: {M: 230, N: 222}, 192 | }, 193 | }, 194 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 195 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, 1.0.2B 196 | 0: {M: 59, N: 51}, 197 | 1: {M: 59, N: 51}, 198 | 2: {M: 59, N: 51}, 199 | 3: {M: 123, N: 115}, 200 | 4: {M: 250, N: 242}, 201 | 5: {M: 250, N: 242}, 202 | }, 203 | }, 204 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 205 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 206 | 0: {M: 59, N: 51}, 207 | 1: {M: 59, N: 51}, 208 | 2: {M: 59, N: 51}, 209 | 3: {M: 123, N: 115}, 210 | 4: {M: 250, N: 242}, 211 | 5: {M: 250, N: 242}, 212 | }, 213 | }, 214 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 215 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 216 | 0: {M: 59, N: 51}, 217 | 1: {M: 59, N: 51}, 218 | 2: {M: 59, N: 51}, 219 | 3: {M: 123, N: 115}, 220 | 4: {M: 250, N: 242}, 221 | 5: {M: 250, N: 242}, 222 | }, 223 | }, 224 | latest: map[string]map[int]MaxPayloadSize{ 225 | RegParamRevRP002_1_0_0: map[int]MaxPayloadSize{ // RP002-1.0.0 226 | 0: {M: 59, N: 51}, 227 | 1: {M: 59, N: 51}, 228 | 2: {M: 59, N: 51}, 229 | 3: {M: 123, N: 115}, 230 | 4: {M: 250, N: 242}, 231 | 5: {M: 250, N: 242}, 232 | }, 233 | latest: map[int]MaxPayloadSize{ // RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 234 | 0: {M: 0, N: 0}, 235 | 1: {M: 31, N: 23}, 236 | 2: {M: 94, N: 86}, 237 | 3: {M: 192, N: 184}, 238 | 4: {M: 250, N: 242}, 239 | 5: {M: 250, N: 242}, 240 | 6: {M: 250, N: 242}, 241 | 7: {M: 250, N: 242}, 242 | }, 243 | }, 244 | } 245 | } 246 | 247 | // initialize uplink channels 248 | for i := uint32(0); i < 96; i++ { 249 | b.uplinkChannels[i] = Channel{ 250 | Frequency: 470300000 + (i * 200000), 251 | MinDR: 0, 252 | MaxDR: 5, 253 | enabled: true, 254 | } 255 | } 256 | 257 | // initialize downlink channels 258 | for i := uint32(0); i < 48; i++ { 259 | b.downlinkChannels[i] = Channel{ 260 | Frequency: 500300000 + (i * 200000), 261 | MinDR: 0, 262 | MaxDR: 5, 263 | enabled: true, 264 | } 265 | } 266 | 267 | return &b, nil 268 | } 269 | -------------------------------------------------------------------------------- /band/band_cn470_510_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestCN470Band(t *testing.T) { 13 | Convey("Given the CN 470-510 band is selected", t, func() { 14 | band, err := GetConfig(CN_470_510, true, lorawan.DwellTimeNoLimit) 15 | So(err, ShouldBeNil) 16 | 17 | Convey("Then GetDefaults returns the expected value", func() { 18 | So(band.GetDefaults(), ShouldResemble, Defaults{ 19 | RX2Frequency: 505300000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | }) 26 | }) 27 | 28 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 29 | So(band.GetDownlinkTXPower(0), ShouldEqual, 14) 30 | }) 31 | 32 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 33 | tests := []struct { 34 | DevAddr lorawan.DevAddr 35 | BeaconTime string 36 | ExpectedFrequency int 37 | }{ 38 | { 39 | DevAddr: lorawan.DevAddr{3, 20, 207, 54}, 40 | BeaconTime: "334382h51m44s", 41 | ExpectedFrequency: 509100000, 42 | }, 43 | } 44 | 45 | for _, test := range tests { 46 | bt, err := time.ParseDuration(test.BeaconTime) 47 | So(err, ShouldBeNil) 48 | freq, err := band.GetPingSlotFrequency(test.DevAddr, bt) 49 | So(err, ShouldBeNil) 50 | So(freq, ShouldEqual, test.ExpectedFrequency) 51 | } 52 | }) 53 | 54 | Convey("When testing the uplink channels", func() { 55 | testTable := []struct { 56 | Channel int 57 | Frequency int 58 | MinDR int 59 | MaxDR int 60 | }{ 61 | {Channel: 0, Frequency: 470300000, MinDR: 0, MaxDR: 5}, 62 | {Channel: 95, Frequency: 489300000, MinDR: 0, MaxDR: 5}, 63 | } 64 | 65 | for _, test := range testTable { 66 | Convey(fmt.Sprintf("Then channel %d must have frequency %d and min/max data-rates %d/%d", test.Channel, test.Frequency, test.MinDR, test.MaxDR), func() { 67 | c, err := band.GetUplinkChannel(test.Channel) 68 | So(err, ShouldBeNil) 69 | So(c.Frequency, ShouldEqual, test.Frequency) 70 | So(c.MinDR, ShouldResemble, test.MinDR) 71 | So(c.MaxDR, ShouldResemble, test.MaxDR) 72 | }) 73 | } 74 | }) 75 | 76 | Convey("When testing the downlink channels", func() { 77 | testTable := []struct { 78 | Frequency uint32 79 | Channel int 80 | ExpFrequency uint32 81 | }{ 82 | {Frequency: 470300000, Channel: 0, ExpFrequency: 500300000}, 83 | {Frequency: 489300000, Channel: 95, ExpFrequency: 509700000}, 84 | } 85 | 86 | for _, test := range testTable { 87 | Convey(fmt.Sprintf("Then frequency: %d must return frequency: %d", test.Frequency, test.ExpFrequency), func() { 88 | txChan, err := band.GetUplinkChannelIndex(test.Frequency, true) 89 | So(err, ShouldBeNil) 90 | So(txChan, ShouldEqual, test.Channel) 91 | 92 | freq, err := band.GetRX1FrequencyForUplinkFrequency(test.Frequency) 93 | So(err, ShouldBeNil) 94 | So(freq, ShouldEqual, test.ExpFrequency) 95 | }) 96 | } 97 | }) 98 | 99 | Convey("When testing LinkADRReqPayload functions", func() { 100 | allChannels := band.GetUplinkChannelIndices() 101 | var filteredChannels []int 102 | 103 | for i := 0; i < 96; i++ { 104 | if i == 6 || i == 38 || i == 45 { 105 | continue 106 | } 107 | filteredChannels = append(filteredChannels, i) 108 | } 109 | 110 | tests := []struct { 111 | Name string 112 | NodeChannels []int 113 | DisableChannels []int 114 | ExpectedUplinkChannels []int 115 | ExpectedLinkADRReqPayloads []lorawan.LinkADRReqPayload 116 | }{ 117 | { 118 | Name: "all channels active", 119 | NodeChannels: band.GetEnabledUplinkChannelIndices(), 120 | ExpectedUplinkChannels: allChannels, 121 | }, 122 | { 123 | Name: "channel 6, 38 and 45 disabled", 124 | NodeChannels: band.GetEnabledUplinkChannelIndices(), 125 | DisableChannels: []int{6, 38, 45}, 126 | ExpectedUplinkChannels: filteredChannels, 127 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 128 | { 129 | ChMask: lorawan.ChMask{true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true}, 130 | Redundancy: lorawan.Redundancy{ChMaskCntl: 0}, 131 | }, 132 | { 133 | ChMask: lorawan.ChMask{true, true, true, true, true, true, false, true, true, true, true, true, true, false, true, true}, 134 | Redundancy: lorawan.Redundancy{ChMaskCntl: 2}, 135 | }, 136 | }, 137 | }, 138 | } 139 | 140 | for i, test := range tests { 141 | Convey(fmt.Sprintf("testing %s [%d]", test.Name, i), func() { 142 | for _, c := range test.DisableChannels { 143 | So(band.DisableUplinkChannelIndex(c), ShouldBeNil) 144 | } 145 | pls := band.GetLinkADRReqPayloadsForEnabledUplinkChannelIndices(test.NodeChannels) 146 | So(pls, ShouldResemble, test.ExpectedLinkADRReqPayloads) 147 | 148 | chans, err := band.GetEnabledUplinkChannelIndicesForLinkADRReqPayloads(test.NodeChannels, pls) 149 | So(err, ShouldBeNil) 150 | So(chans, ShouldResemble, test.ExpectedUplinkChannels) 151 | }) 152 | } 153 | }) 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /band/band_cn779_787.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | type cn779Band struct { 10 | band 11 | } 12 | 13 | func (b *cn779Band) Name() string { 14 | return "CN779" 15 | } 16 | 17 | func (b *cn779Band) GetDefaults() Defaults { 18 | return Defaults{ 19 | RX2Frequency: 786000000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | } 26 | } 27 | 28 | func (b *cn779Band) GetDownlinkTXPower(freq uint32) int { 29 | return 10 30 | } 31 | 32 | func (b *cn779Band) GetDefaultMaxUplinkEIRP() float32 { 33 | return 12.15 34 | } 35 | 36 | func (b *cn779Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 37 | return 785000000, nil 38 | } 39 | 40 | func (b *cn779Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 41 | return uplinkChannel, nil 42 | } 43 | 44 | func (b *cn779Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 45 | return uplinkFrequency, nil 46 | } 47 | 48 | func (b *cn779Band) ImplementsTXParamSetup(protocolVersion string) bool { 49 | return false 50 | } 51 | 52 | func newCN779Band(repeaterCompatible bool) (Band, error) { 53 | b := cn779Band{ 54 | band: band{ 55 | supportsExtraChannels: true, 56 | cFListMinDR: 0, 57 | cFListMaxDR: 5, 58 | dataRates: map[int]DataRate{ 59 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 60 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 61 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 62 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 63 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 64 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 65 | 6: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 250, uplink: true, downlink: true}, 66 | 7: {Modulation: FSKModulation, BitRate: 50000, uplink: true, downlink: true}, 67 | }, 68 | rx1DataRateTable: map[int][]int{ 69 | 0: {0, 0, 0, 0, 0, 0}, 70 | 1: {1, 0, 0, 0, 0, 0}, 71 | 2: {2, 1, 0, 0, 0, 0}, 72 | 3: {3, 2, 1, 0, 0, 0}, 73 | 4: {4, 3, 2, 1, 0, 0}, 74 | 5: {5, 4, 3, 2, 1, 0}, 75 | 6: {6, 5, 4, 3, 2, 1}, 76 | 7: {7, 6, 5, 4, 3, 2}, 77 | }, 78 | txPowerOffsets: []int{ 79 | 0, 80 | -2, 81 | -4, 82 | -6, 83 | -8, 84 | -10, 85 | }, 86 | uplinkChannels: []Channel{ 87 | {Frequency: 779500000, MinDR: 0, MaxDR: 5, enabled: true}, 88 | {Frequency: 779700000, MinDR: 0, MaxDR: 5, enabled: true}, 89 | {Frequency: 779900000, MinDR: 0, MaxDR: 5, enabled: true}, 90 | }, 91 | 92 | downlinkChannels: []Channel{ 93 | {Frequency: 779500000, MinDR: 0, MaxDR: 5, enabled: true}, 94 | {Frequency: 779700000, MinDR: 0, MaxDR: 5, enabled: true}, 95 | {Frequency: 779900000, MinDR: 0, MaxDR: 5, enabled: true}, 96 | }, 97 | }, 98 | } 99 | 100 | if repeaterCompatible { 101 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 102 | LoRaWAN_1_0_0: map[string]map[int]MaxPayloadSize{ 103 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.0 104 | 0: {M: 59, N: 51}, 105 | 1: {M: 59, N: 51}, 106 | 2: {M: 59, N: 51}, 107 | 3: {M: 123, N: 115}, 108 | 4: {M: 230, N: 222}, 109 | 5: {M: 230, N: 222}, 110 | 6: {M: 250, N: 242}, 111 | 7: {M: 230, N: 222}, 112 | }, 113 | }, 114 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 115 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 116 | 0: {M: 59, N: 51}, 117 | 1: {M: 59, N: 51}, 118 | 2: {M: 59, N: 51}, 119 | 3: {M: 123, N: 115}, 120 | 4: {M: 230, N: 222}, 121 | 5: {M: 230, N: 222}, 122 | 6: {M: 250, N: 242}, 123 | 7: {M: 230, N: 222}, 124 | }, 125 | }, 126 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 127 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, LoRaWAN 1.0.2B 128 | 0: {M: 59, N: 51}, 129 | 1: {M: 59, N: 51}, 130 | 2: {M: 59, N: 51}, 131 | 3: {M: 123, N: 115}, 132 | 4: {M: 230, N: 222}, 133 | 5: {M: 230, N: 222}, 134 | 6: {M: 250, N: 242}, 135 | 7: {M: 230, N: 222}, 136 | }, 137 | }, 138 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 139 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 140 | 0: {M: 59, N: 51}, 141 | 1: {M: 59, N: 51}, 142 | 2: {M: 59, N: 51}, 143 | 3: {M: 123, N: 115}, 144 | 4: {M: 230, N: 222}, 145 | 5: {M: 230, N: 222}, 146 | 6: {M: 250, N: 242}, 147 | 7: {M: 230, N: 222}, 148 | }, 149 | }, 150 | latest: map[string]map[int]MaxPayloadSize{ 151 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 152 | 0: {M: 59, N: 51}, 153 | 1: {M: 59, N: 51}, 154 | 2: {M: 59, N: 51}, 155 | 3: {M: 123, N: 115}, 156 | 4: {M: 230, N: 222}, 157 | 5: {M: 230, N: 222}, 158 | 6: {M: 230, N: 222}, 159 | 7: {M: 230, N: 222}, 160 | }, 161 | }, 162 | } 163 | } else { 164 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 165 | LoRaWAN_1_0_0: map[string]map[int]MaxPayloadSize{ 166 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.0 167 | 0: {M: 59, N: 51}, 168 | 1: {M: 59, N: 51}, 169 | 2: {M: 59, N: 51}, 170 | 3: {M: 123, N: 115}, 171 | 4: {M: 250, N: 242}, 172 | 5: {M: 250, N: 242}, 173 | 6: {M: 250, N: 242}, 174 | 7: {M: 250, N: 242}, 175 | }, 176 | }, 177 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 178 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 179 | 0: {M: 59, N: 51}, 180 | 1: {M: 59, N: 51}, 181 | 2: {M: 59, N: 51}, 182 | 3: {M: 123, N: 115}, 183 | 4: {M: 250, N: 242}, 184 | 5: {M: 250, N: 242}, 185 | 6: {M: 250, N: 242}, 186 | 7: {M: 250, N: 242}, 187 | }, 188 | }, 189 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 190 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, LoRaWAN 1.0.2B 191 | 0: {M: 59, N: 51}, 192 | 1: {M: 59, N: 51}, 193 | 2: {M: 59, N: 51}, 194 | 3: {M: 123, N: 115}, 195 | 4: {M: 250, N: 242}, 196 | 5: {M: 250, N: 242}, 197 | 6: {M: 250, N: 242}, 198 | 7: {M: 250, N: 242}, 199 | }, 200 | }, 201 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 202 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 203 | 0: {M: 59, N: 51}, 204 | 1: {M: 59, N: 51}, 205 | 2: {M: 59, N: 51}, 206 | 3: {M: 123, N: 115}, 207 | 4: {M: 250, N: 242}, 208 | 5: {M: 250, N: 242}, 209 | 6: {M: 250, N: 242}, 210 | 7: {M: 250, N: 242}, 211 | }, 212 | }, 213 | latest: map[string]map[int]MaxPayloadSize{ 214 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 215 | 0: {M: 59, N: 51}, 216 | 1: {M: 59, N: 51}, 217 | 2: {M: 59, N: 51}, 218 | 3: {M: 123, N: 115}, 219 | 4: {M: 250, N: 242}, 220 | 5: {M: 250, N: 242}, 221 | 6: {M: 250, N: 242}, 222 | 7: {M: 250, N: 242}, 223 | }, 224 | }, 225 | } 226 | } 227 | 228 | return &b, nil 229 | } 230 | -------------------------------------------------------------------------------- /band/band_cn779_787_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/brocaar/lorawan" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestCN779Band(t *testing.T) { 12 | Convey("Given the CN 779 band is selected with repeaterCompatible=true", t, func() { 13 | band, err := GetConfig(CN_779_787, true, lorawan.DwellTimeNoLimit) 14 | So(err, ShouldBeNil) 15 | 16 | Convey("Then GetDefaults returns the expected value", func() { 17 | So(band.GetDefaults(), ShouldResemble, Defaults{ 18 | RX2Frequency: 786000000, 19 | RX2DataRate: 0, 20 | ReceiveDelay1: time.Second, 21 | ReceiveDelay2: time.Second * 2, 22 | JoinAcceptDelay1: time.Second * 5, 23 | JoinAcceptDelay2: time.Second * 6, 24 | }) 25 | }) 26 | 27 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 28 | So(band.GetDownlinkTXPower(0), ShouldEqual, 10) 29 | }) 30 | 31 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 32 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 33 | So(err, ShouldBeNil) 34 | So(f, ShouldEqual, 785000000) 35 | }) 36 | 37 | Convey("Then GetRX1ChannelIndexForUplinkChannelIndex returns the expected value", func() { 38 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(3) 39 | So(err, ShouldBeNil) 40 | So(c, ShouldEqual, 3) 41 | }) 42 | 43 | Convey("Then GetRX1FrequencyForUplinkFrequency returns the expected value", func() { 44 | f, err := band.GetRX1FrequencyForUplinkFrequency(779500000) 45 | So(err, ShouldBeNil) 46 | So(f, ShouldEqual, 779500000) 47 | }) 48 | 49 | Convey("Then the max payload size (N) is 222 for DR4", func() { 50 | s, err := band.GetMaxPayloadSizeForDataRateIndex(LoRaWAN_1_0_2, RegParamRevB, 4) 51 | So(err, ShouldBeNil) 52 | So(s.N, ShouldEqual, 222) 53 | }) 54 | }) 55 | 56 | Convey("Given the CN 779 band is selected with repeaterCompatible=false", t, func() { 57 | band, err := GetConfig(CN_779_787, false, lorawan.DwellTimeNoLimit) 58 | So(err, ShouldBeNil) 59 | 60 | Convey("Then the max payload size (N) is 242 for DR4", func() { 61 | s, err := band.GetMaxPayloadSizeForDataRateIndex(LoRaWAN_1_0_2, RegParamRevB, 4) 62 | So(err, ShouldBeNil) 63 | So(s.N, ShouldEqual, 242) 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /band/band_eu433.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | type eu443Band struct { 10 | band 11 | } 12 | 13 | func (b *eu443Band) Name() string { 14 | return "EU433" 15 | } 16 | 17 | func (b *eu443Band) GetDefaults() Defaults { 18 | return Defaults{ 19 | RX2Frequency: 434665000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | } 26 | } 27 | 28 | func (b *eu443Band) GetDownlinkTXPower(freq uint32) int { 29 | return 10 30 | } 31 | 32 | func (b *eu443Band) GetDefaultMaxUplinkEIRP() float32 { 33 | return 12.15 34 | } 35 | 36 | func (b *eu443Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 37 | return 434665000, nil 38 | } 39 | 40 | func (b *eu443Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 41 | return uplinkChannel, nil 42 | } 43 | 44 | func (b *eu443Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 45 | return uplinkFrequency, nil 46 | } 47 | 48 | func (b *eu443Band) ImplementsTXParamSetup(protocolVersion string) bool { 49 | return false 50 | } 51 | 52 | func newEU433Band(repeaterCompatible bool) (Band, error) { 53 | b := eu443Band{ 54 | band: band{ 55 | supportsExtraChannels: true, 56 | cFListMinDR: 0, 57 | cFListMaxDR: 5, 58 | dataRates: map[int]DataRate{ 59 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 60 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 61 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 62 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 63 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 64 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 65 | 6: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 250, uplink: true, downlink: true}, 66 | 7: {Modulation: FSKModulation, BitRate: 50000, uplink: true, downlink: true}, 67 | }, 68 | rx1DataRateTable: map[int][]int{ 69 | 0: {0, 0, 0, 0, 0, 0}, 70 | 1: {1, 0, 0, 0, 0, 0}, 71 | 2: {2, 1, 0, 0, 0, 0}, 72 | 3: {3, 2, 1, 0, 0, 0}, 73 | 4: {4, 3, 2, 1, 0, 0}, 74 | 5: {5, 4, 3, 2, 1, 0}, 75 | 6: {6, 5, 4, 3, 2, 1}, 76 | 7: {7, 6, 5, 4, 3, 2}, 77 | }, 78 | txPowerOffsets: []int{ 79 | 0, 80 | -2, 81 | -4, 82 | -6, 83 | -8, 84 | -10, 85 | }, 86 | uplinkChannels: []Channel{ 87 | {Frequency: 433175000, MinDR: 0, MaxDR: 5, enabled: true}, 88 | {Frequency: 433375000, MinDR: 0, MaxDR: 5, enabled: true}, 89 | {Frequency: 433575000, MinDR: 0, MaxDR: 5, enabled: true}, 90 | }, 91 | 92 | downlinkChannels: []Channel{ 93 | {Frequency: 433175000, MinDR: 0, MaxDR: 5, enabled: true}, 94 | {Frequency: 433375000, MinDR: 0, MaxDR: 5, enabled: true}, 95 | {Frequency: 433575000, MinDR: 0, MaxDR: 5, enabled: true}, 96 | }, 97 | }, 98 | } 99 | 100 | if repeaterCompatible { 101 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 102 | LoRaWAN_1_0_0: map[string]map[int]MaxPayloadSize{ 103 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.0 104 | 0: {M: 59, N: 51}, 105 | 1: {M: 59, N: 51}, 106 | 2: {M: 59, N: 51}, 107 | 3: {M: 123, N: 115}, 108 | 4: {M: 230, N: 222}, 109 | 5: {M: 230, N: 222}, 110 | 6: {M: 230, N: 222}, 111 | 7: {M: 230, N: 222}, 112 | }, 113 | }, 114 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 115 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 116 | 0: {M: 59, N: 51}, 117 | 1: {M: 59, N: 51}, 118 | 2: {M: 59, N: 51}, 119 | 3: {M: 123, N: 115}, 120 | 4: {M: 230, N: 222}, 121 | 5: {M: 230, N: 222}, 122 | 6: {M: 230, N: 222}, 123 | 7: {M: 230, N: 222}, 124 | }, 125 | }, 126 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 127 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, 1.0.2B 128 | 0: {M: 59, N: 51}, 129 | 1: {M: 59, N: 51}, 130 | 2: {M: 59, N: 51}, 131 | 3: {M: 123, N: 115}, 132 | 4: {M: 230, N: 222}, 133 | 5: {M: 230, N: 222}, 134 | 6: {M: 230, N: 222}, 135 | 7: {M: 230, N: 222}, 136 | }, 137 | }, 138 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 139 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 140 | 0: {M: 59, N: 51}, 141 | 1: {M: 59, N: 51}, 142 | 2: {M: 59, N: 51}, 143 | 3: {M: 123, N: 115}, 144 | 4: {M: 230, N: 222}, 145 | 5: {M: 230, N: 222}, 146 | 6: {M: 230, N: 222}, 147 | 7: {M: 230, N: 222}, 148 | }, 149 | }, 150 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 151 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 152 | 0: {M: 59, N: 51}, 153 | 1: {M: 59, N: 51}, 154 | 2: {M: 59, N: 51}, 155 | 3: {M: 123, N: 115}, 156 | 4: {M: 230, N: 222}, 157 | 5: {M: 230, N: 222}, 158 | 6: {M: 230, N: 222}, 159 | 7: {M: 230, N: 222}, 160 | }, 161 | }, 162 | latest: map[string]map[int]MaxPayloadSize{ 163 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 164 | 0: {M: 59, N: 51}, 165 | 1: {M: 59, N: 51}, 166 | 2: {M: 59, N: 51}, 167 | 3: {M: 123, N: 115}, 168 | 4: {M: 230, N: 222}, 169 | 5: {M: 230, N: 222}, 170 | 6: {M: 230, N: 222}, 171 | 7: {M: 230, N: 222}, 172 | }, 173 | }, 174 | } 175 | } else { 176 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 177 | LoRaWAN_1_0_0: map[string]map[int]MaxPayloadSize{ 178 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.0 179 | 0: {M: 59, N: 51}, 180 | 1: {M: 59, N: 51}, 181 | 2: {M: 59, N: 51}, 182 | 3: {M: 123, N: 115}, 183 | 4: {M: 250, N: 242}, 184 | 5: {M: 250, N: 242}, 185 | 6: {M: 250, N: 242}, 186 | 7: {M: 250, N: 242}, 187 | }, 188 | }, 189 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 190 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 191 | 0: {M: 59, N: 51}, 192 | 1: {M: 59, N: 51}, 193 | 2: {M: 59, N: 51}, 194 | 3: {M: 123, N: 115}, 195 | 4: {M: 250, N: 242}, 196 | 5: {M: 250, N: 242}, 197 | 6: {M: 250, N: 242}, 198 | 7: {M: 250, N: 242}, 199 | }, 200 | }, 201 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 202 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, 1.0.2B 203 | 0: {M: 59, N: 51}, 204 | 1: {M: 59, N: 51}, 205 | 2: {M: 59, N: 51}, 206 | 3: {M: 123, N: 115}, 207 | 4: {M: 250, N: 242}, 208 | 5: {M: 250, N: 242}, 209 | 6: {M: 250, N: 242}, 210 | 7: {M: 250, N: 242}, 211 | }, 212 | }, 213 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 214 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 215 | 0: {M: 59, N: 51}, 216 | 1: {M: 59, N: 51}, 217 | 2: {M: 59, N: 51}, 218 | 3: {M: 123, N: 115}, 219 | 4: {M: 250, N: 242}, 220 | 5: {M: 250, N: 242}, 221 | 6: {M: 250, N: 242}, 222 | 7: {M: 250, N: 242}, 223 | }, 224 | }, 225 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 226 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 227 | 0: {M: 59, N: 51}, 228 | 1: {M: 59, N: 51}, 229 | 2: {M: 59, N: 51}, 230 | 3: {M: 123, N: 115}, 231 | 4: {M: 250, N: 242}, 232 | 5: {M: 250, N: 242}, 233 | 6: {M: 250, N: 242}, 234 | 7: {M: 250, N: 242}, 235 | }, 236 | }, 237 | latest: map[string]map[int]MaxPayloadSize{ 238 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 239 | 0: {M: 59, N: 51}, 240 | 1: {M: 59, N: 51}, 241 | 2: {M: 59, N: 51}, 242 | 3: {M: 123, N: 115}, 243 | 4: {M: 250, N: 242}, 244 | 5: {M: 250, N: 242}, 245 | 6: {M: 250, N: 242}, 246 | 7: {M: 250, N: 242}, 247 | }, 248 | }, 249 | } 250 | } 251 | 252 | return &b, nil 253 | } 254 | -------------------------------------------------------------------------------- /band/band_eu433_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/brocaar/lorawan" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestEU433Band(t *testing.T) { 12 | Convey("Given the EU 433 band is selected with repeaterCompatible=true", t, func() { 13 | band, err := GetConfig(EU_433, true, lorawan.DwellTimeNoLimit) 14 | So(err, ShouldBeNil) 15 | 16 | Convey("Then GetDefaults returns the expected value", func() { 17 | So(band.GetDefaults(), ShouldResemble, Defaults{ 18 | RX2Frequency: 434665000, 19 | RX2DataRate: 0, 20 | ReceiveDelay1: time.Second, 21 | ReceiveDelay2: time.Second * 2, 22 | JoinAcceptDelay1: time.Second * 5, 23 | JoinAcceptDelay2: time.Second * 6, 24 | }) 25 | }) 26 | 27 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 28 | So(band.GetDownlinkTXPower(0), ShouldEqual, 10) 29 | }) 30 | 31 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 32 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 33 | So(err, ShouldBeNil) 34 | So(f, ShouldEqual, 434665000) 35 | }) 36 | 37 | Convey("Then GetRX1ChannelIndexForUplinkChannelIndex returns the exepcted value", func() { 38 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(3) 39 | So(err, ShouldBeNil) 40 | So(c, ShouldEqual, 3) 41 | }) 42 | 43 | Convey("Then GetRX1FrequencyForUplinkFrequency returns the expected value", func() { 44 | f, err := band.GetRX1FrequencyForUplinkFrequency(433175000) 45 | So(err, ShouldBeNil) 46 | So(f, ShouldEqual, 433175000) 47 | }) 48 | 49 | Convey("Then the max payload size (N) is 222 for DR4", func() { 50 | s, err := band.GetMaxPayloadSizeForDataRateIndex(LoRaWAN_1_0_2, RegParamRevB, 4) 51 | So(err, ShouldBeNil) 52 | So(s.N, ShouldEqual, 222) 53 | }) 54 | }) 55 | 56 | Convey("Given the EU 433 band is selected with repeaterCompatible=false", t, func() { 57 | band, err := GetConfig(EU_433, false, lorawan.DwellTimeNoLimit) 58 | So(err, ShouldBeNil) 59 | 60 | Convey("Then the max payload size (N) is 242 for DR4", func() { 61 | s, err := band.GetMaxPayloadSizeForDataRateIndex(LoRaWAN_1_0_2, RegParamRevB, 4) 62 | So(err, ShouldBeNil) 63 | So(s.N, ShouldEqual, 242) 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /band/band_eu863_870.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | type eu863Band struct { 10 | band 11 | } 12 | 13 | func (b *eu863Band) Name() string { 14 | return "EU868" 15 | } 16 | 17 | func (b *eu863Band) GetDefaults() Defaults { 18 | return Defaults{ 19 | RX2Frequency: 869525000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | } 26 | } 27 | 28 | func (b *eu863Band) GetDownlinkTXPower(freq uint32) int { 29 | // NOTE: as there are currently no further boundary checks on the frequency, this check is sufficient. 30 | // TODO: However, there should be some mechanism, that checks the frequency for compliance to regulations. 31 | if 863000000 <= freq && freq < 869200000 { 32 | return 14 //25mW 33 | } else if 869400000 <= freq && freq < 869650000 { 34 | return 27 //500mW 35 | } else { 36 | return 14 // Default case 37 | } 38 | } 39 | 40 | func (b *eu863Band) GetDefaultMaxUplinkEIRP() float32 { 41 | return 16 42 | } 43 | 44 | func (b *eu863Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 45 | return 869525000, nil 46 | } 47 | 48 | func (b *eu863Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 49 | return uplinkChannel, nil 50 | } 51 | 52 | func (b *eu863Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 53 | return uplinkFrequency, nil 54 | } 55 | 56 | func (b *eu863Band) ImplementsTXParamSetup(protocolVersion string) bool { 57 | return false 58 | } 59 | 60 | func newEU863Band(repeatedCompatible bool) (Band, error) { 61 | b := eu863Band{ 62 | band: band{ 63 | supportsExtraChannels: true, 64 | cFListMinDR: 0, 65 | cFListMaxDR: 5, 66 | dataRates: map[int]DataRate{ 67 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 68 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 69 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 70 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 71 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 72 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 73 | 6: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 250, uplink: true, downlink: true}, 74 | 7: {Modulation: FSKModulation, BitRate: 50000, uplink: true, downlink: true}, 75 | 8: {Modulation: LRFHSSModulation, CodingRate: "1/3", OccupiedChannelWidth: 137000, uplink: true, downlink: false}, 76 | 9: {Modulation: LRFHSSModulation, CodingRate: "4/6", OccupiedChannelWidth: 137000, uplink: true, downlink: false}, 77 | 10: {Modulation: LRFHSSModulation, CodingRate: "1/3", OccupiedChannelWidth: 336000, uplink: true, downlink: false}, 78 | 11: {Modulation: LRFHSSModulation, CodingRate: "4/6", OccupiedChannelWidth: 336000, uplink: true, downlink: false}, 79 | }, 80 | rx1DataRateTable: map[int][]int{ 81 | 0: {0, 0, 0, 0, 0, 0}, 82 | 1: {1, 0, 0, 0, 0, 0}, 83 | 2: {2, 1, 0, 0, 0, 0}, 84 | 3: {3, 2, 1, 0, 0, 0}, 85 | 4: {4, 3, 2, 1, 0, 0}, 86 | 5: {5, 4, 3, 2, 1, 0}, 87 | 6: {6, 5, 4, 3, 2, 1}, 88 | 7: {7, 6, 5, 4, 3, 2}, 89 | 8: {1, 0, 0, 0, 0, 0}, 90 | 9: {2, 1, 0, 0, 0, 0}, 91 | 10: {1, 0, 0, 0, 0, 0}, 92 | 11: {2, 1, 0, 0, 0, 0}, 93 | }, 94 | txPowerOffsets: []int{ 95 | 0, 96 | -2, 97 | -4, 98 | -6, 99 | -8, 100 | -10, 101 | -12, 102 | -14, 103 | }, 104 | uplinkChannels: []Channel{ 105 | {Frequency: 868100000, MinDR: 0, MaxDR: 5, enabled: true}, 106 | {Frequency: 868300000, MinDR: 0, MaxDR: 5, enabled: true}, 107 | {Frequency: 868500000, MinDR: 0, MaxDR: 5, enabled: true}, 108 | }, 109 | downlinkChannels: []Channel{ 110 | {Frequency: 868100000, MinDR: 0, MaxDR: 5, enabled: true}, 111 | {Frequency: 868300000, MinDR: 0, MaxDR: 5, enabled: true}, 112 | {Frequency: 868500000, MinDR: 0, MaxDR: 5, enabled: true}, 113 | }, 114 | }, 115 | } 116 | 117 | if repeatedCompatible { 118 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 119 | LoRaWAN_1_0_0: map[string]map[int]MaxPayloadSize{ 120 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.0 121 | 0: {M: 59, N: 51}, 122 | 1: {M: 59, N: 51}, 123 | 2: {M: 59, N: 51}, 124 | 3: {M: 123, N: 115}, 125 | 4: {M: 230, N: 222}, 126 | 5: {M: 230, N: 222}, 127 | 6: {M: 230, N: 222}, 128 | 7: {M: 230, N: 222}, 129 | }, 130 | }, 131 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 132 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 133 | 0: {M: 59, N: 51}, 134 | 1: {M: 59, N: 51}, 135 | 2: {M: 59, N: 51}, 136 | 3: {M: 123, N: 115}, 137 | 4: {M: 230, N: 222}, 138 | 5: {M: 230, N: 222}, 139 | 6: {M: 230, N: 222}, 140 | 7: {M: 230, N: 222}, 141 | }, 142 | }, 143 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 144 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, 1.0.2B 145 | 0: {M: 59, N: 51}, 146 | 1: {M: 59, N: 51}, 147 | 2: {M: 59, N: 51}, 148 | 3: {M: 123, N: 115}, 149 | 4: {M: 230, N: 222}, 150 | 5: {M: 230, N: 222}, 151 | 6: {M: 230, N: 222}, 152 | 7: {M: 230, N: 222}, 153 | }, 154 | }, 155 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 156 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 157 | 0: {M: 59, N: 51}, 158 | 1: {M: 59, N: 51}, 159 | 2: {M: 59, N: 51}, 160 | 3: {M: 123, N: 115}, 161 | 4: {M: 230, N: 222}, 162 | 5: {M: 230, N: 222}, 163 | 6: {M: 230, N: 222}, 164 | 7: {M: 230, N: 222}, 165 | }, 166 | }, 167 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 168 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 169 | 0: {M: 59, N: 51}, 170 | 1: {M: 59, N: 51}, 171 | 2: {M: 59, N: 51}, 172 | 3: {M: 123, N: 115}, 173 | 4: {M: 230, N: 222}, 174 | 5: {M: 230, N: 222}, 175 | 6: {M: 230, N: 222}, 176 | 7: {M: 230, N: 222}, 177 | }, 178 | }, 179 | latest: map[string]map[int]MaxPayloadSize{ 180 | RegParamRevRP002_1_0_0: map[int]MaxPayloadSize{ 181 | 0: {M: 59, N: 51}, 182 | 1: {M: 59, N: 51}, 183 | 2: {M: 59, N: 51}, 184 | 3: {M: 123, N: 115}, 185 | 4: {M: 230, N: 222}, 186 | 5: {M: 230, N: 222}, 187 | 6: {M: 230, N: 222}, 188 | 7: {M: 230, N: 222}, 189 | }, 190 | RegParamRevRP002_1_0_1: map[int]MaxPayloadSize{ 191 | 0: {M: 59, N: 51}, 192 | 1: {M: 59, N: 51}, 193 | 2: {M: 59, N: 51}, 194 | 3: {M: 123, N: 115}, 195 | 4: {M: 230, N: 222}, 196 | 5: {M: 230, N: 222}, 197 | 6: {M: 230, N: 222}, 198 | 7: {M: 230, N: 222}, 199 | }, 200 | latest: map[int]MaxPayloadSize{ // RP002-1.0.2, RP002-1.0.3 201 | 0: {M: 59, N: 51}, 202 | 1: {M: 59, N: 51}, 203 | 2: {M: 59, N: 51}, 204 | 3: {M: 123, N: 115}, 205 | 4: {M: 230, N: 222}, 206 | 5: {M: 230, N: 222}, 207 | 6: {M: 230, N: 222}, 208 | 7: {M: 230, N: 222}, 209 | 8: {M: 58, N: 50}, 210 | 9: {M: 123, N: 115}, 211 | 10: {M: 58, N: 50}, 212 | 11: {M: 123, N: 115}, 213 | }, 214 | }, 215 | } 216 | } else { 217 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 218 | LoRaWAN_1_0_0: map[string]map[int]MaxPayloadSize{ 219 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.0 220 | 0: {M: 59, N: 51}, 221 | 1: {M: 59, N: 51}, 222 | 2: {M: 59, N: 51}, 223 | 3: {M: 123, N: 115}, 224 | 4: {M: 250, N: 242}, 225 | 5: {M: 250, N: 242}, 226 | 6: {M: 250, N: 242}, 227 | 7: {M: 250, N: 242}, 228 | }, 229 | }, 230 | LoRaWAN_1_0_1: map[string]map[int]MaxPayloadSize{ 231 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.1 232 | 0: {M: 59, N: 51}, 233 | 1: {M: 59, N: 51}, 234 | 2: {M: 59, N: 51}, 235 | 3: {M: 123, N: 115}, 236 | 4: {M: 250, N: 242}, 237 | 5: {M: 250, N: 242}, 238 | 6: {M: 250, N: 242}, 239 | 7: {M: 250, N: 242}, 240 | }, 241 | }, 242 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 243 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2A, 1.0.2B 244 | 0: {M: 59, N: 51}, 245 | 1: {M: 59, N: 51}, 246 | 2: {M: 59, N: 51}, 247 | 3: {M: 123, N: 115}, 248 | 4: {M: 250, N: 242}, 249 | 5: {M: 250, N: 242}, 250 | 6: {M: 250, N: 242}, 251 | 7: {M: 250, N: 242}, 252 | }, 253 | }, 254 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 255 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 256 | 0: {M: 59, N: 51}, 257 | 1: {M: 59, N: 51}, 258 | 2: {M: 59, N: 51}, 259 | 3: {M: 123, N: 115}, 260 | 4: {M: 250, N: 242}, 261 | 5: {M: 250, N: 242}, 262 | 6: {M: 250, N: 242}, 263 | 7: {M: 250, N: 242}, 264 | }, 265 | }, 266 | latest: map[string]map[int]MaxPayloadSize{ 267 | RegParamRevRP002_1_0_0: map[int]MaxPayloadSize{ 268 | 0: {M: 59, N: 51}, 269 | 1: {M: 59, N: 51}, 270 | 2: {M: 59, N: 51}, 271 | 3: {M: 123, N: 115}, 272 | 4: {M: 250, N: 242}, 273 | 5: {M: 250, N: 242}, 274 | 6: {M: 250, N: 242}, 275 | 7: {M: 250, N: 242}, 276 | }, 277 | RegParamRevRP002_1_0_1: map[int]MaxPayloadSize{ 278 | 0: {M: 59, N: 51}, 279 | 1: {M: 59, N: 51}, 280 | 2: {M: 59, N: 51}, 281 | 3: {M: 123, N: 115}, 282 | 4: {M: 250, N: 242}, 283 | 5: {M: 250, N: 242}, 284 | 6: {M: 250, N: 242}, 285 | 7: {M: 250, N: 242}, 286 | }, 287 | latest: map[int]MaxPayloadSize{ // RP002-1.0.2, RP002-1.0.3 288 | 0: {M: 59, N: 51}, 289 | 1: {M: 59, N: 51}, 290 | 2: {M: 59, N: 51}, 291 | 3: {M: 123, N: 115}, 292 | 4: {M: 250, N: 242}, 293 | 5: {M: 250, N: 242}, 294 | 6: {M: 250, N: 242}, 295 | 7: {M: 250, N: 242}, 296 | 8: {M: 58, N: 50}, 297 | 9: {M: 123, N: 115}, 298 | 10: {M: 58, N: 50}, 299 | 11: {M: 123, N: 115}, 300 | }, 301 | }, 302 | } 303 | } 304 | 305 | return &b, nil 306 | } 307 | -------------------------------------------------------------------------------- /band/band_eu863_870_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestEU863Band(t *testing.T) { 13 | Convey("Given the EU 863-870 band is selected", t, func() { 14 | band, err := GetConfig(EU_863_870, true, lorawan.DwellTimeNoLimit) 15 | So(err, ShouldBeNil) 16 | 17 | Convey("Then GetDefaults returns the expected value", func() { 18 | So(band.GetDefaults(), ShouldResemble, Defaults{ 19 | RX2Frequency: 869525000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | }) 26 | }) 27 | 28 | Convey("Then the expected enabled uplink data-rates are returned", func() { 29 | upDR := band.GetEnabledUplinkDataRates() 30 | So(upDR, ShouldResemble, []int{0, 1, 2, 3, 4, 5}) 31 | }) 32 | 33 | Convey("Then GetDownlinkTXPower returns the expected value for 863.000000 MHz", func() { 34 | So(band.GetDownlinkTXPower(863000000), ShouldEqual, 14) 35 | }) 36 | 37 | Convey("Then GetDownlinkTXPower returns the expected value for 863.000001 MHz", func() { 38 | So(band.GetDownlinkTXPower(863000001), ShouldEqual, 14) 39 | }) 40 | 41 | Convey("Then GetDownlinkTXPower returns the expected value for 869.200000 MHz", func() { 42 | So(band.GetDownlinkTXPower(869200000), ShouldEqual, 14) 43 | }) 44 | 45 | Convey("Then GetDownlinkTXPower returns the expected value for 869.400000 MHz", func() { 46 | So(band.GetDownlinkTXPower(869400000), ShouldEqual, 27) 47 | }) 48 | 49 | Convey("Then GetDownlinkTXPower returns the expected value for 869.400001 MHz", func() { 50 | So(band.GetDownlinkTXPower(869400001), ShouldEqual, 27) 51 | }) 52 | 53 | Convey("Then GetDownlinkTXPower returns the expected value for 869.650000 MHz", func() { 54 | So(band.GetDownlinkTXPower(869650000), ShouldEqual, 14) 55 | }) 56 | 57 | Convey("Then GetDownlinkTXPower returns the expected value for any other value (0)", func() { 58 | So(band.GetDownlinkTXPower(0), ShouldEqual, 14) 59 | }) 60 | 61 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 62 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 63 | So(err, ShouldBeNil) 64 | So(f, ShouldEqual, 869525000) 65 | }) 66 | 67 | Convey("Then GetRX1ChannelIndexForUplinkChannelIndex returns the expected value", func() { 68 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(3) 69 | So(err, ShouldBeNil) 70 | So(c, ShouldEqual, 3) 71 | }) 72 | 73 | Convey("Then GetRX1FrequencyForUplinkFrequency returns the expected value", func() { 74 | f, err := band.GetRX1FrequencyForUplinkFrequency(868500000) 75 | So(err, ShouldBeNil) 76 | So(f, ShouldEqual, 868500000) 77 | }) 78 | 79 | Convey("Then GetDataRateIndex returns the exepected values", func() { 80 | tests := []struct { 81 | DataRate DataRate 82 | Uplink bool 83 | ExpectedDR int 84 | }{ 85 | { 86 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125}, 87 | Uplink: true, 88 | ExpectedDR: 0, 89 | }, 90 | { 91 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125}, 92 | Uplink: false, 93 | ExpectedDR: 0, 94 | }, 95 | { 96 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125}, 97 | Uplink: true, 98 | ExpectedDR: 5, 99 | }, 100 | { 101 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125}, 102 | Uplink: false, 103 | ExpectedDR: 5, 104 | }, 105 | { 106 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 250}, 107 | Uplink: true, 108 | ExpectedDR: 6, 109 | }, 110 | { 111 | DataRate: DataRate{Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 250}, 112 | Uplink: false, 113 | ExpectedDR: 6, 114 | }, 115 | { 116 | DataRate: DataRate{Modulation: LRFHSSModulation, CodingRate: "1/3", OccupiedChannelWidth: 137000}, 117 | Uplink: true, 118 | ExpectedDR: 8, 119 | }, 120 | { 121 | DataRate: DataRate{Modulation: LRFHSSModulation, CodingRate: "4/6", OccupiedChannelWidth: 336000}, 122 | Uplink: true, 123 | ExpectedDR: 11, 124 | }, 125 | } 126 | 127 | for _, t := range tests { 128 | dr, err := band.GetDataRateIndex(t.Uplink, t.DataRate) 129 | So(err, ShouldBeNil) 130 | So(dr, ShouldEqual, t.ExpectedDR) 131 | } 132 | }) 133 | 134 | Convey("Given five extra channels", func() { 135 | chans := []uint32{ 136 | 867100000, 137 | 867300000, 138 | 867500000, 139 | 867700000, 140 | 867900000, 141 | } 142 | 143 | for _, c := range chans { 144 | band.AddChannel(c, 0, 5) 145 | } 146 | 147 | Convey("Then these are returned as custom channels", func() { 148 | So(band.GetCustomUplinkChannelIndices(), ShouldResemble, []int{3, 4, 5, 6, 7}) 149 | }) 150 | 151 | Convey("When testing the LinkADRReqPayload functions", func() { 152 | tests := []struct { 153 | Name string 154 | NodeChannels []int 155 | DisabledChannels []int 156 | ExpectedUplinkChannels []int 157 | ExpectedLinkADRReqPayloads []lorawan.LinkADRReqPayload 158 | }{ 159 | { 160 | Name: "no active node channels", 161 | NodeChannels: []int{}, 162 | ExpectedUplinkChannels: []int{0, 1, 2}, 163 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 164 | { 165 | ChMask: lorawan.ChMask{true, true, true}, 166 | }, 167 | }, 168 | // we only activate the base channels 169 | }, 170 | { 171 | Name: "base channels are active", 172 | NodeChannels: []int{0, 1, 2}, 173 | ExpectedUplinkChannels: []int{0, 1, 2}, 174 | // we do not activate the CFList channels as we don't 175 | // now if the node knows about these frequencies 176 | }, 177 | { 178 | Name: "base channels + two CFList channels are active", 179 | NodeChannels: []int{0, 1, 2, 3, 4}, 180 | ExpectedUplinkChannels: []int{0, 1, 2, 3, 4}, 181 | // we do not activate the CFList channels as we don't 182 | // now if the node knows about these frequencies 183 | }, 184 | { 185 | Name: "base channels + CFList are active", 186 | NodeChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 187 | ExpectedUplinkChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 188 | // nothing to do, network and node are in sync 189 | }, 190 | { 191 | Name: "base channels + CFList are active on node, but CFList channels are disabled on the network", 192 | NodeChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 193 | DisabledChannels: []int{3, 4, 5, 6, 7}, 194 | ExpectedUplinkChannels: []int{0, 1, 2}, 195 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 196 | { 197 | ChMask: lorawan.ChMask{true, true, true}, 198 | }, 199 | }, 200 | // we disable the CFList channels as they became inactive 201 | }, 202 | } 203 | 204 | for i, test := range tests { 205 | Convey(fmt.Sprintf("testing %s [%d]", test.Name, i), func() { 206 | for _, c := range test.DisabledChannels { 207 | So(band.DisableUplinkChannelIndex(c), ShouldBeNil) 208 | } 209 | pls := band.GetLinkADRReqPayloadsForEnabledUplinkChannelIndices(test.NodeChannels) 210 | So(pls, ShouldResemble, test.ExpectedLinkADRReqPayloads) 211 | 212 | chans, err := band.GetEnabledUplinkChannelIndicesForLinkADRReqPayloads(test.NodeChannels, pls) 213 | So(err, ShouldBeNil) 214 | So(chans, ShouldResemble, test.ExpectedUplinkChannels) 215 | }) 216 | } 217 | 218 | }) 219 | 220 | Convey("Then GetUplinkChannelIndex takes the extra channels into consideration", func() { 221 | tests := []uint32{ 222 | 868100000, 223 | 868300000, 224 | 868500000, 225 | 867100000, 226 | 867300000, 227 | 867500000, 228 | 867700000, 229 | 867900000, 230 | } 231 | 232 | for expChannel, expFreq := range tests { 233 | var defaultChannel bool 234 | if expChannel < 3 { 235 | defaultChannel = true 236 | } 237 | channel, err := band.GetUplinkChannelIndex(expFreq, defaultChannel) 238 | So(err, ShouldBeNil) 239 | So(channel, ShouldEqual, expChannel) 240 | } 241 | }) 242 | 243 | Convey("Then GetUplinkChannelIndexForFrequencyDR takes the extra channels into consideration", func() { 244 | tests := []uint32{ 245 | 868100000, 246 | 868300000, 247 | 868500000, 248 | 867100000, 249 | 867300000, 250 | 867500000, 251 | 867700000, 252 | 867900000, 253 | } 254 | 255 | for expChannel, freq := range tests { 256 | channel, err := band.GetUplinkChannelIndexForFrequencyDR(freq, 3) 257 | So(err, ShouldBeNil) 258 | So(channel, ShouldEqual, expChannel) 259 | } 260 | }) 261 | 262 | Convey("Then GetCFList returns the expected CFList", func() { 263 | cFList := band.GetCFList(LoRaWAN_1_0_2) 264 | So(cFList, ShouldNotBeNil) 265 | So(cFList, ShouldResemble, &lorawan.CFList{ 266 | CFListType: lorawan.CFListChannel, 267 | Payload: &lorawan.CFListChannelPayload{ 268 | Channels: [5]uint32{ 269 | 867100000, 270 | 867300000, 271 | 867500000, 272 | 867700000, 273 | 867900000, 274 | }, 275 | }, 276 | }) 277 | }) 278 | }) 279 | }) 280 | } 281 | -------------------------------------------------------------------------------- /band/band_in_865_867.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | type in865Band struct { 10 | band 11 | } 12 | 13 | func (b *in865Band) Name() string { 14 | return "IN865" 15 | } 16 | 17 | func (b *in865Band) GetDefaults() Defaults { 18 | return Defaults{ 19 | RX2Frequency: 866550000, 20 | RX2DataRate: 2, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | } 26 | } 27 | 28 | func (b *in865Band) GetDownlinkTXPower(freq uint32) int { 29 | return 27 30 | } 31 | 32 | func (b *in865Band) GetDefaultMaxUplinkEIRP() float32 { 33 | return 30 34 | } 35 | 36 | func (b *in865Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 37 | return 866550000, nil 38 | } 39 | 40 | func (b *in865Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 41 | return uplinkChannel, nil 42 | } 43 | 44 | func (b *in865Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 45 | return uplinkFrequency, nil 46 | } 47 | 48 | func (b *in865Band) ImplementsTXParamSetup(protocolVersion string) bool { 49 | return false 50 | } 51 | 52 | func newIN865Band(repeaterCompatible bool) (Band, error) { 53 | b := in865Band{ 54 | band: band{ 55 | supportsExtraChannels: true, 56 | cFListMinDR: 0, 57 | cFListMaxDR: 5, 58 | dataRates: map[int]DataRate{ 59 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 60 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 61 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 62 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 63 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 64 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 65 | // 6 66 | 7: {Modulation: FSKModulation, BitRate: 50000, uplink: true, downlink: true}, 67 | }, 68 | rx1DataRateTable: map[int][]int{ 69 | 0: {0, 0, 0, 0, 0, 0, 1, 2}, 70 | 1: {1, 0, 0, 0, 0, 0, 2, 3}, 71 | 2: {2, 1, 0, 0, 0, 0, 3, 4}, 72 | 3: {3, 2, 1, 0, 0, 0, 4, 5}, 73 | 4: {4, 3, 2, 1, 0, 0, 5, 5}, 74 | 5: {5, 4, 3, 2, 1, 0, 5, 5}, 75 | // 6 76 | 7: {7, 6, 5, 4, 3, 2, 7, 7}, 77 | }, 78 | txPowerOffsets: []int{ 79 | 0, 80 | -2, 81 | -4, 82 | -6, 83 | -8, 84 | -10, 85 | -12, 86 | -14, 87 | -16, 88 | -18, 89 | -20, 90 | }, 91 | uplinkChannels: []Channel{ 92 | {Frequency: 865062500, MinDR: 0, MaxDR: 5, enabled: true}, 93 | {Frequency: 865402500, MinDR: 0, MaxDR: 5, enabled: true}, 94 | {Frequency: 865985000, MinDR: 0, MaxDR: 5, enabled: true}, 95 | }, 96 | 97 | downlinkChannels: []Channel{ 98 | {Frequency: 865062500, MinDR: 0, MaxDR: 5, enabled: true}, 99 | {Frequency: 865402500, MinDR: 0, MaxDR: 5, enabled: true}, 100 | {Frequency: 865985000, MinDR: 0, MaxDR: 5, enabled: true}, 101 | }, 102 | }, 103 | } 104 | 105 | if repeaterCompatible { 106 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 107 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 108 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2B 109 | 0: {M: 59, N: 51}, 110 | 1: {M: 59, N: 51}, 111 | 2: {M: 59, N: 51}, 112 | 3: {M: 123, N: 115}, 113 | 4: {M: 230, N: 222}, 114 | 5: {M: 230, N: 222}, 115 | 6: {M: 230, N: 222}, 116 | 7: {M: 230, N: 222}, 117 | }, 118 | }, 119 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 120 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 121 | 0: {M: 59, N: 51}, 122 | 1: {M: 59, N: 51}, 123 | 2: {M: 59, N: 51}, 124 | 3: {M: 123, N: 115}, 125 | 4: {M: 230, N: 222}, 126 | 5: {M: 230, N: 222}, 127 | 6: {M: 230, N: 222}, 128 | 7: {M: 230, N: 222}, 129 | }, 130 | }, 131 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 132 | latest: map[int]MaxPayloadSize{ // 1.1.0A, 1.1.0B 133 | 0: {M: 59, N: 51}, 134 | 1: {M: 59, N: 51}, 135 | 2: {M: 59, N: 51}, 136 | 3: {M: 123, N: 115}, 137 | 4: {M: 230, N: 222}, 138 | 5: {M: 230, N: 222}, 139 | 6: {M: 230, N: 222}, 140 | 7: {M: 230, N: 222}, 141 | }, 142 | }, 143 | latest: map[string]map[int]MaxPayloadSize{ 144 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 145 | 0: {M: 59, N: 51}, 146 | 1: {M: 59, N: 51}, 147 | 2: {M: 59, N: 51}, 148 | 3: {M: 123, N: 115}, 149 | 4: {M: 230, N: 222}, 150 | 5: {M: 230, N: 222}, 151 | 6: {M: 230, N: 222}, 152 | 7: {M: 230, N: 222}, 153 | }, 154 | }, 155 | } 156 | } else { 157 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 158 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 159 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2B 160 | 0: {M: 59, N: 51}, 161 | 1: {M: 59, N: 51}, 162 | 2: {M: 59, N: 51}, 163 | 3: {M: 123, N: 115}, 164 | 4: {M: 250, N: 242}, 165 | 5: {M: 250, N: 242}, 166 | 6: {M: 250, N: 242}, 167 | 7: {M: 250, N: 242}, 168 | }, 169 | }, 170 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 171 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 172 | 0: {M: 59, N: 51}, 173 | 1: {M: 59, N: 51}, 174 | 2: {M: 59, N: 51}, 175 | 3: {M: 123, N: 115}, 176 | 4: {M: 250, N: 242}, 177 | 5: {M: 250, N: 242}, 178 | 6: {M: 250, N: 242}, 179 | 7: {M: 250, N: 242}, 180 | }, 181 | }, 182 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 183 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 184 | 0: {M: 59, N: 51}, 185 | 1: {M: 59, N: 51}, 186 | 2: {M: 59, N: 51}, 187 | 3: {M: 123, N: 115}, 188 | 4: {M: 250, N: 242}, 189 | 5: {M: 250, N: 242}, 190 | 6: {M: 250, N: 242}, 191 | 7: {M: 250, N: 242}, 192 | }, 193 | }, 194 | latest: map[string]map[int]MaxPayloadSize{ 195 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 196 | 0: {M: 59, N: 51}, 197 | 1: {M: 59, N: 51}, 198 | 2: {M: 59, N: 51}, 199 | 3: {M: 123, N: 115}, 200 | 4: {M: 250, N: 242}, 201 | 5: {M: 250, N: 242}, 202 | 6: {M: 250, N: 242}, 203 | 7: {M: 250, N: 242}, 204 | }, 205 | }, 206 | } 207 | } 208 | 209 | return &b, nil 210 | } 211 | -------------------------------------------------------------------------------- /band/band_in_865_867_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/brocaar/lorawan" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestIN865Band(t *testing.T) { 12 | Convey("Given the IN 865 band is selected with repeaterCompatible=true", t, func() { 13 | band, err := GetConfig(IN_865_867, true, lorawan.DwellTimeNoLimit) 14 | So(err, ShouldBeNil) 15 | 16 | Convey("Then GetDefaults returns the expected value", func() { 17 | So(band.GetDefaults(), ShouldResemble, Defaults{ 18 | RX2Frequency: 866550000, 19 | RX2DataRate: 2, 20 | ReceiveDelay1: time.Second, 21 | ReceiveDelay2: time.Second * 2, 22 | JoinAcceptDelay1: time.Second * 5, 23 | JoinAcceptDelay2: time.Second * 6, 24 | }) 25 | }) 26 | 27 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 28 | So(band.GetDownlinkTXPower(0), ShouldEqual, 27) 29 | }) 30 | 31 | Convey("Then GetPingSlotFrequency returns the exepected value", func() { 32 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 33 | So(err, ShouldBeNil) 34 | So(f, ShouldEqual, 866550000) 35 | }) 36 | 37 | Convey("Then GetRX1ChannelIndexForUplinkChannelIndex returns the expected value", func() { 38 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(2) 39 | So(err, ShouldBeNil) 40 | So(c, ShouldEqual, 2) 41 | }) 42 | 43 | Convey("Then RX1FrequencyForUplinkFrequency returns the expected value", func() { 44 | f, err := band.GetRX1FrequencyForUplinkFrequency(866550000) 45 | So(err, ShouldBeNil) 46 | So(f, ShouldEqual, 866550000) 47 | }) 48 | 49 | Convey("Then the max payload size (N) is 222 for DR4", func() { 50 | s, err := band.GetMaxPayloadSizeForDataRateIndex(LoRaWAN_1_0_2, RegParamRevB, 4) 51 | So(err, ShouldBeNil) 52 | So(s.N, ShouldEqual, 222) 53 | }) 54 | }) 55 | 56 | Convey("Given the IN 865 band is selected with repeaterCompatible=false", t, func() { 57 | band, err := GetConfig(IN_865_867, false, lorawan.DwellTimeNoLimit) 58 | So(err, ShouldBeNil) 59 | 60 | Convey("Then the max payload size (N) is 242 for DR4", func() { 61 | s, err := band.GetMaxPayloadSizeForDataRateIndex(LoRaWAN_1_0_2, RegParamRevB, 4) 62 | So(err, ShouldBeNil) 63 | So(s.N, ShouldEqual, 242) 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /band/band_ism2400.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | // see: https://lora-developers.semtech.com/library/tech-papers-and-guides/physical-layer-proposal-2.4ghz 10 | type ism2400Band struct { 11 | band 12 | } 13 | 14 | func (b *ism2400Band) Name() string { 15 | return "ISM2400" 16 | } 17 | 18 | func (b *ism2400Band) GetDefaults() Defaults { 19 | return Defaults{ 20 | RX2Frequency: 2423000000, 21 | RX2DataRate: 0, 22 | ReceiveDelay1: time.Second, 23 | ReceiveDelay2: time.Second * 2, 24 | JoinAcceptDelay1: time.Second * 5, 25 | JoinAcceptDelay2: time.Second * 6, 26 | } 27 | } 28 | 29 | func (b *ism2400Band) GetDownlinkTXPower(freq uint32) int { 30 | return 10 31 | } 32 | 33 | func (b *ism2400Band) GetDefaultMaxUplinkEIRP() float32 { 34 | return 10 35 | } 36 | 37 | func (b *ism2400Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 38 | return 2424000000, nil 39 | } 40 | 41 | func (b *ism2400Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 42 | return uplinkChannel, nil 43 | } 44 | 45 | func (b *ism2400Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 46 | return uplinkFrequency, nil 47 | } 48 | 49 | func (b *ism2400Band) ImplementsTXParamSetup(protocolVersion string) bool { 50 | return true 51 | } 52 | 53 | func newISM2400Band(repeaterCompatible bool) (Band, error) { 54 | b := ism2400Band{ 55 | band: band{ 56 | supportsExtraChannels: true, 57 | cFListMinDR: 0, 58 | cFListMaxDR: 7, 59 | dataRates: map[int]DataRate{ 60 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 812, uplink: true, downlink: true}, 61 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 812, uplink: true, downlink: true}, 62 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 812, uplink: true, downlink: true}, 63 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 812, uplink: true, downlink: true}, 64 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 812, uplink: true, downlink: true}, 65 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 812, uplink: true, downlink: true}, 66 | 6: {Modulation: LoRaModulation, SpreadFactor: 6, Bandwidth: 812, uplink: true, downlink: true}, 67 | 7: {Modulation: LoRaModulation, SpreadFactor: 5, Bandwidth: 812, uplink: true, downlink: true}, 68 | }, 69 | rx1DataRateTable: map[int][]int{ 70 | 0: {0, 0, 0, 0, 0, 0}, 71 | 1: {1, 0, 0, 0, 0, 0}, 72 | 2: {2, 1, 0, 0, 0, 0}, 73 | 3: {3, 2, 1, 0, 0, 0}, 74 | 4: {4, 3, 2, 1, 0, 0}, 75 | 5: {5, 4, 2, 2, 1, 0}, 76 | 6: {6, 5, 4, 3, 2, 1}, 77 | 7: {7, 6, 5, 4, 3, 2}, 78 | }, 79 | txPowerOffsets: []int{ 80 | 0, 81 | -2, 82 | -4, 83 | -6, 84 | -8, 85 | -10, 86 | -12, 87 | -14, 88 | }, 89 | uplinkChannels: []Channel{ 90 | {Frequency: 2403000000, MinDR: 0, MaxDR: 7, enabled: true}, 91 | {Frequency: 2425000000, MinDR: 0, MaxDR: 7, enabled: true}, 92 | {Frequency: 2479000000, MinDR: 0, MaxDR: 7, enabled: true}, 93 | }, 94 | downlinkChannels: []Channel{ 95 | {Frequency: 2403000000, MinDR: 0, MaxDR: 7, enabled: true}, 96 | {Frequency: 2425000000, MinDR: 0, MaxDR: 7, enabled: true}, 97 | {Frequency: 2479000000, MinDR: 0, MaxDR: 7, enabled: true}, 98 | }, 99 | }, 100 | } 101 | 102 | if repeaterCompatible { 103 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 104 | latest: map[string]map[int]MaxPayloadSize{ 105 | latest: map[int]MaxPayloadSize{ 106 | 0: {M: 59, N: 51}, 107 | 1: {M: 123, N: 115}, 108 | 2: {M: 228, N: 220}, 109 | 3: {M: 228, N: 220}, 110 | 4: {M: 228, N: 220}, 111 | 5: {M: 228, N: 220}, 112 | 6: {M: 228, N: 220}, 113 | 7: {M: 228, N: 220}, 114 | }, 115 | }, 116 | } 117 | } else { 118 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 119 | latest: map[string]map[int]MaxPayloadSize{ 120 | latest: map[int]MaxPayloadSize{ 121 | 0: {M: 59, N: 51}, 122 | 1: {M: 123, N: 115}, 123 | 2: {M: 248, N: 220}, 124 | 3: {M: 248, N: 240}, 125 | 4: {M: 248, N: 240}, 126 | 5: {M: 248, N: 240}, 127 | 6: {M: 248, N: 240}, 128 | 7: {M: 248, N: 240}, 129 | }, 130 | }, 131 | } 132 | } 133 | 134 | return &b, nil 135 | } 136 | -------------------------------------------------------------------------------- /band/band_ism2400_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/brocaar/lorawan" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestISM2400Band(t *testing.T) { 12 | assert := require.New(t) 13 | 14 | band, err := GetConfig(ISM2400, true, lorawan.DwellTimeNoLimit) 15 | assert.NoError(err) 16 | 17 | t.Run("GetDefaults", func(t *testing.T) { 18 | assert := require.New(t) 19 | assert.Equal(Defaults{ 20 | RX2Frequency: 2423000000, 21 | RX2DataRate: 0, 22 | ReceiveDelay1: time.Second, 23 | ReceiveDelay2: time.Second * 2, 24 | JoinAcceptDelay1: time.Second * 5, 25 | JoinAcceptDelay2: time.Second * 6, 26 | }, band.GetDefaults()) 27 | }) 28 | 29 | t.Run("GetDownlinkTXPower", func(t *testing.T) { 30 | assert := require.New(t) 31 | assert.Equal(10, band.GetDownlinkTXPower(2423000000)) 32 | }) 33 | 34 | t.Run("GetPingSlotFrequency", func(t *testing.T) { 35 | assert := require.New(t) 36 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 37 | assert.NoError(err) 38 | assert.EqualValues(2424000000, f) 39 | 40 | }) 41 | 42 | t.Run("GetRX1ChannelIndexForUplinkChannelIndex", func(t *testing.T) { 43 | assert := require.New(t) 44 | i, err := band.GetRX1ChannelIndexForUplinkChannelIndex(3) 45 | assert.NoError(err) 46 | assert.Equal(3, i) 47 | }) 48 | 49 | t.Run("GetRX1FrequencyForUplinkFrequency", func(t *testing.T) { 50 | assert := require.New(t) 51 | f, err := band.GetRX1FrequencyForUplinkFrequency(2425000000) 52 | assert.NoError(err) 53 | assert.EqualValues(2425000000, f) 54 | }) 55 | 56 | t.Run("Five extra channels", func(t *testing.T) { 57 | chans := []uint32{ 58 | 2426000000, 59 | 2427000000, 60 | 2428000000, 61 | 2429000000, 62 | 2430000000, 63 | } 64 | 65 | for _, c := range chans { 66 | band.AddChannel(c, 0, 7) 67 | } 68 | 69 | t.Run("GetCustomUplinkChannelIndices", func(t *testing.T) { 70 | assert := require.New(t) 71 | assert.Equal([]int{3, 4, 5, 6, 7}, band.GetCustomUplinkChannelIndices()) 72 | }) 73 | 74 | t.Run("Test LinkADRReqPayload", func(t *testing.T) { 75 | tests := []struct { 76 | Name string 77 | NodeChannels []int 78 | DisabledChannels []int 79 | ExpectedUplinkChannels []int 80 | ExpectedLinkADRReqPayloads []lorawan.LinkADRReqPayload 81 | }{ 82 | { 83 | Name: "no active node channels", 84 | NodeChannels: []int{}, 85 | ExpectedUplinkChannels: []int{0, 1, 2}, 86 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 87 | { 88 | ChMask: lorawan.ChMask{true, true, true}, 89 | }, 90 | }, 91 | // we only activate the base channels 92 | }, 93 | { 94 | Name: "base channels are active", 95 | NodeChannels: []int{0, 1, 2}, 96 | ExpectedUplinkChannels: []int{0, 1, 2}, 97 | // we do not activate the CFList channels as we don't 98 | // now if the node knows about these frequencies 99 | }, 100 | { 101 | Name: "base channels + two CFList channels are active", 102 | NodeChannels: []int{0, 1, 2, 3, 4}, 103 | ExpectedUplinkChannels: []int{0, 1, 2, 3, 4}, 104 | // we do not activate the CFList channels as we don't 105 | // now if the node knows about these frequencies 106 | }, 107 | { 108 | Name: "base channels + CFList are active", 109 | NodeChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 110 | ExpectedUplinkChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 111 | // nothing to do, network and node are in sync 112 | }, 113 | { 114 | Name: "base channels + CFList are active on node, but CFList channels are disabled on the network", 115 | NodeChannels: []int{0, 1, 2, 3, 4, 5, 6, 7}, 116 | DisabledChannels: []int{3, 4, 5, 6, 7}, 117 | ExpectedUplinkChannels: []int{0, 1, 2}, 118 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 119 | { 120 | ChMask: lorawan.ChMask{true, true, true}, 121 | }, 122 | }, 123 | // we disable the CFList channels as they became inactive 124 | }, 125 | } 126 | 127 | for _, tst := range tests { 128 | t.Run(tst.Name, func(t *testing.T) { 129 | assert := require.New(t) 130 | for _, i := range tst.DisabledChannels { 131 | assert.NoError(band.DisableUplinkChannelIndex(i)) 132 | } 133 | 134 | pls := band.GetLinkADRReqPayloadsForEnabledUplinkChannelIndices(tst.NodeChannels) 135 | assert.Equal(tst.ExpectedLinkADRReqPayloads, pls) 136 | 137 | chans, err := band.GetEnabledUplinkChannelIndicesForLinkADRReqPayloads(tst.NodeChannels, pls) 138 | assert.NoError(err) 139 | assert.Equal(tst.ExpectedUplinkChannels, chans) 140 | }) 141 | } 142 | }) 143 | 144 | t.Run("GetUplinkChannelIndex", func(t *testing.T) { 145 | tests := []uint32{ 146 | 2403000000, 147 | 2425000000, 148 | 2479000000, 149 | 2426000000, 150 | 2427000000, 151 | 2428000000, 152 | 2429000000, 153 | 2430000000, 154 | } 155 | 156 | for expChannel, expFreq := range tests { 157 | var defaultChannel bool 158 | if expChannel < 3 { 159 | defaultChannel = true 160 | } 161 | 162 | channel, err := band.GetUplinkChannelIndex(expFreq, defaultChannel) 163 | assert.NoError(err) 164 | assert.Equal(expChannel, channel) 165 | } 166 | }) 167 | 168 | t.Run("GetUplinkChannelIndexForFrequencyDR", func(t *testing.T) { 169 | tests := []uint32{ 170 | 2403000000, 171 | 2425000000, 172 | 2479000000, 173 | 2426000000, 174 | 2427000000, 175 | 2428000000, 176 | 2429000000, 177 | 2430000000, 178 | } 179 | 180 | for expChannel, freq := range tests { 181 | channel, err := band.GetUplinkChannelIndexForFrequencyDR(freq, 3) 182 | assert.NoError(err) 183 | assert.Equal(expChannel, channel) 184 | } 185 | }) 186 | 187 | t.Run("GetCFList", func(t *testing.T) { 188 | assert := require.New(t) 189 | cFList := band.GetCFList(LoRaWAN_1_0_4) 190 | assert.NotNil(cFList) 191 | assert.EqualValues(&lorawan.CFList{ 192 | CFListType: lorawan.CFListChannel, 193 | Payload: &lorawan.CFListChannelPayload{ 194 | Channels: [5]uint32{ 195 | 2426000000, 196 | 2427000000, 197 | 2428000000, 198 | 2429000000, 199 | 2430000000, 200 | }, 201 | }, 202 | }, cFList) 203 | }) 204 | }) 205 | } 206 | -------------------------------------------------------------------------------- /band/band_kr920_923.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | type kr920Band struct { 10 | band 11 | } 12 | 13 | func (b *kr920Band) Name() string { 14 | return "KR920" 15 | } 16 | 17 | func (b *kr920Band) GetDefaults() Defaults { 18 | return Defaults{ 19 | RX2Frequency: 921900000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | } 26 | } 27 | 28 | func (b *kr920Band) GetDownlinkTXPower(freq uint32) int { 29 | return 23 30 | } 31 | 32 | func (b *kr920Band) GetDefaultMaxUplinkEIRP() float32 { 33 | return 14 34 | } 35 | 36 | func (b *kr920Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 37 | return 923100000, nil 38 | } 39 | 40 | func (b *kr920Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 41 | return uplinkChannel, nil 42 | } 43 | 44 | func (b *kr920Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 45 | return uplinkFrequency, nil 46 | } 47 | 48 | func (b *kr920Band) ImplementsTXParamSetup(protocolVersion string) bool { 49 | return false 50 | } 51 | 52 | func newKR920Band(repeaterCompatible bool) (Band, error) { 53 | b := kr920Band{ 54 | band: band{ 55 | supportsExtraChannels: true, 56 | cFListMinDR: 0, 57 | cFListMaxDR: 5, 58 | dataRates: map[int]DataRate{ 59 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 60 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 61 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 62 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 63 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 64 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 65 | }, 66 | rx1DataRateTable: map[int][]int{ 67 | 0: {0, 0, 0, 0, 0, 0, 1, 2}, 68 | 1: {1, 0, 0, 0, 0, 0, 2, 3}, 69 | 2: {2, 1, 0, 0, 0, 0, 3, 4}, 70 | 3: {3, 2, 1, 0, 0, 0, 4, 5}, 71 | 4: {4, 3, 2, 1, 0, 0, 5, 5}, 72 | 5: {5, 4, 3, 2, 1, 0, 5, 7}, 73 | 6: {0, 0, 0, 0, 0, 0, 0, 0}, 74 | 7: {7, 5, 5, 4, 3, 2, 7, 7}, 75 | }, 76 | txPowerOffsets: []int{ 77 | 0, 78 | -2, 79 | -4, 80 | -6, 81 | -8, 82 | -10, 83 | -12, 84 | -14, 85 | }, 86 | uplinkChannels: []Channel{ 87 | {Frequency: 922100000, MinDR: 0, MaxDR: 5, enabled: true}, 88 | {Frequency: 922300000, MinDR: 0, MaxDR: 5, enabled: true}, 89 | {Frequency: 922500000, MinDR: 0, MaxDR: 5, enabled: true}, 90 | }, 91 | 92 | downlinkChannels: []Channel{ 93 | {Frequency: 922100000, MinDR: 0, MaxDR: 5, enabled: true}, 94 | {Frequency: 922300000, MinDR: 0, MaxDR: 5, enabled: true}, 95 | {Frequency: 922500000, MinDR: 0, MaxDR: 5, enabled: true}, 96 | }, 97 | }, 98 | } 99 | 100 | if repeaterCompatible { 101 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 102 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 103 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2B 104 | 0: {M: 59, N: 51}, 105 | 1: {M: 59, N: 51}, 106 | 2: {M: 59, N: 51}, 107 | 3: {M: 123, N: 115}, 108 | 4: {M: 230, N: 222}, 109 | 5: {M: 230, N: 222}, 110 | }, 111 | }, 112 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 113 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 114 | 0: {M: 59, N: 51}, 115 | 1: {M: 59, N: 51}, 116 | 2: {M: 59, N: 51}, 117 | 3: {M: 123, N: 115}, 118 | 4: {M: 230, N: 222}, 119 | 5: {M: 230, N: 222}, 120 | }, 121 | }, 122 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 123 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 124 | 0: {M: 59, N: 51}, 125 | 1: {M: 59, N: 51}, 126 | 2: {M: 59, N: 51}, 127 | 3: {M: 123, N: 115}, 128 | 4: {M: 230, N: 222}, 129 | 5: {M: 230, N: 222}, 130 | }, 131 | }, 132 | latest: map[string]map[int]MaxPayloadSize{ 133 | RegParamRevRP002_1_0_0: map[int]MaxPayloadSize{ // RP002-1.0.0 134 | 0: {M: 59, N: 51}, 135 | 1: {M: 59, N: 51}, 136 | 2: {M: 59, N: 51}, 137 | 3: {M: 123, N: 115}, 138 | 4: {M: 230, N: 222}, 139 | 5: {M: 230, N: 222}, 140 | }, 141 | latest: map[int]MaxPayloadSize{ // RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 142 | 0: {M: 59, N: 51}, 143 | 1: {M: 59, N: 51}, 144 | 2: {M: 59, N: 51}, 145 | 3: {M: 123, N: 115}, 146 | 4: {M: 230, N: 222}, 147 | 5: {M: 230, N: 222}, 148 | }, 149 | }, 150 | } 151 | } else { 152 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 153 | LoRaWAN_1_0_2: map[string]map[int]MaxPayloadSize{ 154 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.2B 155 | 0: {M: 59, N: 51}, 156 | 1: {M: 59, N: 51}, 157 | 2: {M: 59, N: 51}, 158 | 3: {M: 123, N: 115}, 159 | 4: {M: 250, N: 242}, 160 | 5: {M: 250, N: 242}, 161 | }, 162 | }, 163 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 164 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 165 | 0: {M: 59, N: 51}, 166 | 1: {M: 59, N: 51}, 167 | 2: {M: 59, N: 51}, 168 | 3: {M: 123, N: 115}, 169 | 4: {M: 250, N: 242}, 170 | 5: {M: 250, N: 242}, 171 | }, 172 | }, 173 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 174 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0A, 1.1.0B 175 | 0: {M: 59, N: 51}, 176 | 1: {M: 59, N: 51}, 177 | 2: {M: 59, N: 51}, 178 | 3: {M: 123, N: 115}, 179 | 4: {M: 250, N: 242}, 180 | 5: {M: 250, N: 242}, 181 | }, 182 | }, 183 | latest: map[string]map[int]MaxPayloadSize{ 184 | RegParamRevRP002_1_0_0: map[int]MaxPayloadSize{ // RP002-1.0.0 185 | 0: {M: 59, N: 51}, 186 | 1: {M: 59, N: 51}, 187 | 2: {M: 59, N: 51}, 188 | 3: {M: 123, N: 115}, 189 | 4: {M: 250, N: 242}, 190 | 5: {M: 250, N: 242}, 191 | }, 192 | latest: map[int]MaxPayloadSize{ // RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 193 | 0: {M: 59, N: 51}, 194 | 1: {M: 59, N: 51}, 195 | 2: {M: 59, N: 51}, 196 | 3: {M: 123, N: 115}, 197 | 4: {M: 250, N: 242}, 198 | 5: {M: 250, N: 242}, 199 | }, 200 | }, 201 | } 202 | } 203 | 204 | return &b, nil 205 | } 206 | -------------------------------------------------------------------------------- /band/band_kr920_923_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/brocaar/lorawan" 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestKR920Band(t *testing.T) { 12 | Convey("Given the KR 920 band is selected", t, func() { 13 | band, err := GetConfig(KR_920_923, true, lorawan.DwellTimeNoLimit) 14 | So(err, ShouldBeNil) 15 | 16 | Convey("Then GetDefaults returns the expected value", func() { 17 | So(band.GetDefaults(), ShouldResemble, Defaults{ 18 | RX2Frequency: 921900000, 19 | RX2DataRate: 0, 20 | ReceiveDelay1: time.Second, 21 | ReceiveDelay2: time.Second * 2, 22 | JoinAcceptDelay1: time.Second * 5, 23 | JoinAcceptDelay2: time.Second * 6, 24 | }) 25 | }) 26 | 27 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 28 | So(band.GetDownlinkTXPower(0), ShouldEqual, 23) 29 | }) 30 | 31 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 32 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 33 | So(err, ShouldBeNil) 34 | So(f, ShouldEqual, 923100000) 35 | }) 36 | 37 | Convey("Then GetRX1ChannelIndexForUplinkChannelIndex returns the expected value", func() { 38 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(2) 39 | So(err, ShouldBeNil) 40 | So(c, ShouldEqual, 2) 41 | }) 42 | 43 | Convey("Then GetRX1FrequencyForUplinkFrequency returns the expected value", func() { 44 | f, err := band.GetRX1FrequencyForUplinkFrequency(922100000) 45 | So(err, ShouldBeNil) 46 | So(f, ShouldEqual, 922100000) 47 | }) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /band/band_ru864_870.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/brocaar/lorawan" 7 | ) 8 | 9 | type ru864Band struct { 10 | band 11 | } 12 | 13 | func (b *ru864Band) Name() string { 14 | return "RU864" 15 | } 16 | 17 | func (b *ru864Band) GetDefaults() Defaults { 18 | return Defaults{ 19 | RX2Frequency: 869100000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | } 26 | } 27 | 28 | func (b *ru864Band) GetDownlinkTXPower(freq uint32) int { 29 | return 14 30 | } 31 | 32 | func (b *ru864Band) GetDefaultMaxUplinkEIRP() float32 { 33 | return 16 34 | } 35 | 36 | func (b *ru864Band) GetPingSlotFrequency(lorawan.DevAddr, time.Duration) (uint32, error) { 37 | return 868900000, nil 38 | } 39 | 40 | func (b *ru864Band) GetRX1ChannelIndexForUplinkChannelIndex(uplinkChannel int) (int, error) { 41 | return uplinkChannel, nil 42 | } 43 | 44 | func (b *ru864Band) GetRX1FrequencyForUplinkFrequency(uplinkFrequency uint32) (uint32, error) { 45 | return uplinkFrequency, nil 46 | } 47 | 48 | func (b *ru864Band) ImplementsTXParamSetup(protocolVersion string) bool { 49 | return false 50 | } 51 | 52 | func newRU864Band(repeaterCompatible bool) (Band, error) { 53 | b := ru864Band{ 54 | band: band{ 55 | supportsExtraChannels: true, 56 | cFListMinDR: 0, 57 | cFListMaxDR: 5, 58 | dataRates: map[int]DataRate{ 59 | 0: {Modulation: LoRaModulation, SpreadFactor: 12, Bandwidth: 125, uplink: true, downlink: true}, 60 | 1: {Modulation: LoRaModulation, SpreadFactor: 11, Bandwidth: 125, uplink: true, downlink: true}, 61 | 2: {Modulation: LoRaModulation, SpreadFactor: 10, Bandwidth: 125, uplink: true, downlink: true}, 62 | 3: {Modulation: LoRaModulation, SpreadFactor: 9, Bandwidth: 125, uplink: true, downlink: true}, 63 | 4: {Modulation: LoRaModulation, SpreadFactor: 8, Bandwidth: 125, uplink: true, downlink: true}, 64 | 5: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 125, uplink: true, downlink: true}, 65 | 6: {Modulation: LoRaModulation, SpreadFactor: 7, Bandwidth: 250, uplink: true, downlink: true}, 66 | 7: {Modulation: FSKModulation, BitRate: 50000, uplink: true, downlink: true}, 67 | }, 68 | rx1DataRateTable: map[int][]int{ 69 | 0: {0, 0, 0, 0, 0, 0}, 70 | 1: {1, 0, 0, 0, 0, 0}, 71 | 2: {2, 1, 0, 0, 0, 0}, 72 | 3: {3, 2, 1, 0, 0, 0}, 73 | 4: {4, 3, 2, 1, 0, 0}, 74 | 5: {5, 4, 3, 2, 1, 0}, 75 | 6: {6, 5, 4, 3, 2, 1}, 76 | 7: {7, 6, 5, 4, 3, 2}, 77 | }, 78 | txPowerOffsets: []int{ 79 | 0, 80 | -2, 81 | -4, 82 | -6, 83 | -8, 84 | -10, 85 | -12, 86 | -14, 87 | }, 88 | uplinkChannels: []Channel{ 89 | {Frequency: 868900000, MinDR: 0, MaxDR: 5, enabled: true}, 90 | {Frequency: 869100000, MinDR: 0, MaxDR: 5, enabled: true}, 91 | }, 92 | 93 | downlinkChannels: []Channel{ 94 | {Frequency: 868900000, MinDR: 0, MaxDR: 5, enabled: true}, 95 | {Frequency: 869100000, MinDR: 0, MaxDR: 5, enabled: true}, 96 | }, 97 | }, 98 | } 99 | 100 | if repeaterCompatible { 101 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 102 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 103 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3A 104 | 0: {M: 59, N: 51}, 105 | 1: {M: 59, N: 51}, 106 | 2: {M: 59, N: 51}, 107 | 3: {M: 123, N: 115}, 108 | 4: {M: 230, N: 222}, 109 | 5: {M: 230, N: 222}, 110 | 6: {M: 230, N: 222}, 111 | 7: {M: 230, N: 222}, 112 | }, 113 | }, 114 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 115 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0B 116 | 0: {M: 59, N: 51}, 117 | 1: {M: 59, N: 51}, 118 | 2: {M: 59, N: 51}, 119 | 3: {M: 123, N: 115}, 120 | 4: {M: 230, N: 222}, 121 | 5: {M: 230, N: 222}, 122 | 6: {M: 230, N: 222}, 123 | 7: {M: 230, N: 222}, 124 | }, 125 | }, 126 | latest: map[string]map[int]MaxPayloadSize{ 127 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 128 | 0: {M: 59, N: 51}, 129 | 1: {M: 59, N: 51}, 130 | 2: {M: 59, N: 51}, 131 | 3: {M: 123, N: 115}, 132 | 4: {M: 230, N: 222}, 133 | 5: {M: 230, N: 222}, 134 | 6: {M: 230, N: 222}, 135 | 7: {M: 230, N: 222}, 136 | }, 137 | }, 138 | } 139 | } else { 140 | b.band.maxPayloadSizePerDR = map[string]map[string]map[int]MaxPayloadSize{ 141 | LoRaWAN_1_0_3: map[string]map[int]MaxPayloadSize{ 142 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.0.3B 143 | 0: {M: 59, N: 51}, 144 | 1: {M: 59, N: 51}, 145 | 2: {M: 59, N: 51}, 146 | 3: {M: 123, N: 115}, 147 | 4: {M: 250, N: 242}, 148 | 5: {M: 250, N: 242}, 149 | 6: {M: 250, N: 242}, 150 | 7: {M: 250, N: 242}, 151 | }, 152 | }, 153 | LoRaWAN_1_1_0: map[string]map[int]MaxPayloadSize{ 154 | latest: map[int]MaxPayloadSize{ // LoRaWAN 1.1.0B 155 | 0: {M: 59, N: 51}, 156 | 1: {M: 59, N: 51}, 157 | 2: {M: 59, N: 51}, 158 | 3: {M: 123, N: 115}, 159 | 4: {M: 250, N: 242}, 160 | 5: {M: 250, N: 242}, 161 | 6: {M: 250, N: 242}, 162 | 7: {M: 250, N: 242}, 163 | }, 164 | }, 165 | latest: map[string]map[int]MaxPayloadSize{ 166 | latest: map[int]MaxPayloadSize{ // RP002-1.0.0, RP002-1.0.1, RP002-1.0.2, RP002-1.0.3 167 | 0: {M: 59, N: 51}, 168 | 1: {M: 59, N: 51}, 169 | 2: {M: 59, N: 51}, 170 | 3: {M: 123, N: 115}, 171 | 4: {M: 250, N: 242}, 172 | 5: {M: 250, N: 242}, 173 | 6: {M: 250, N: 242}, 174 | 7: {M: 250, N: 242}, 175 | }, 176 | }, 177 | } 178 | } 179 | 180 | return &b, nil 181 | } 182 | -------------------------------------------------------------------------------- /band/band_ru864_870_test.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/brocaar/lorawan" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestRU864Band(t *testing.T) { 13 | Convey("Given the RU 864-869 band is selected", t, func() { 14 | band, err := GetConfig(RU_864_870, true, lorawan.DwellTimeNoLimit) 15 | So(err, ShouldBeNil) 16 | 17 | Convey("Then GetDefaults returns the expected value", func() { 18 | So(band.GetDefaults(), ShouldResemble, Defaults{ 19 | RX2Frequency: 869100000, 20 | RX2DataRate: 0, 21 | ReceiveDelay1: time.Second, 22 | ReceiveDelay2: time.Second * 2, 23 | JoinAcceptDelay1: time.Second * 5, 24 | JoinAcceptDelay2: time.Second * 6, 25 | }) 26 | }) 27 | 28 | Convey("Then GetDownlinkTXPower returns the expected value", func() { 29 | So(band.GetDownlinkTXPower(0), ShouldEqual, 14) 30 | }) 31 | 32 | Convey("Then GetPingSlotFrequency returns the expected value", func() { 33 | f, err := band.GetPingSlotFrequency(lorawan.DevAddr{}, 0) 34 | So(err, ShouldBeNil) 35 | So(f, ShouldEqual, 868900000) 36 | }) 37 | 38 | Convey("Then GetRX1ChannelIndexForUplinkChannelIndex returns the expected value", func() { 39 | c, err := band.GetRX1ChannelIndexForUplinkChannelIndex(3) 40 | So(err, ShouldBeNil) 41 | So(c, ShouldEqual, 3) 42 | }) 43 | 44 | Convey("Then GetRX1FrequencyForUplinkFrequency returns the expected value", func() { 45 | f, err := band.GetRX1FrequencyForUplinkFrequency(868900000) 46 | So(err, ShouldBeNil) 47 | So(f, ShouldEqual, 868900000) 48 | }) 49 | 50 | Convey("Given five extra channels", func() { 51 | chans := []uint32{ 52 | 864100000, 53 | 864300000, 54 | 864500000, 55 | 864700000, 56 | 864900000, 57 | } 58 | 59 | for _, c := range chans { 60 | band.AddChannel(c, 0, 5) 61 | } 62 | 63 | Convey("Then these are returned as custom channels", func() { 64 | So(band.GetCustomUplinkChannelIndices(), ShouldResemble, []int{2, 3, 4, 5, 6}) 65 | }) 66 | 67 | Convey("When testing the LinkADRReqPayload functions", func() { 68 | tests := []struct { 69 | Name string 70 | NodeChannels []int 71 | DisabledChannels []int 72 | ExpectedUplinkChannels []int 73 | ExpectedLinkADRReqPayloads []lorawan.LinkADRReqPayload 74 | }{ 75 | { 76 | Name: "no active node channels", 77 | NodeChannels: []int{}, 78 | ExpectedUplinkChannels: []int{0, 1}, 79 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 80 | { 81 | ChMask: lorawan.ChMask{true, true}, 82 | }, 83 | }, 84 | // we only activate the base channels 85 | }, 86 | { 87 | Name: "base channels are active", 88 | NodeChannels: []int{0, 1}, 89 | ExpectedUplinkChannels: []int{0, 1}, 90 | // we do not activate the CFList channels as we don't 91 | // now if the node knows about these frequencies 92 | }, 93 | { 94 | Name: "base channels + two CFList channels are active", 95 | NodeChannels: []int{0, 1, 2, 3}, 96 | ExpectedUplinkChannels: []int{0, 1, 2, 3}, 97 | // we do not activate the CFList channels as we don't 98 | // now if the node knows about these frequencies 99 | }, 100 | { 101 | Name: "base channels + CFList are active", 102 | NodeChannels: []int{0, 1, 2, 3, 4, 5, 6}, 103 | ExpectedUplinkChannels: []int{0, 1, 2, 3, 4, 5, 6}, 104 | // nothing to do, network and node are in sync 105 | }, 106 | { 107 | Name: "base channels + CFList are active on node, but CFList channels are disabled on the network", 108 | NodeChannels: []int{0, 1, 2, 3, 4, 5, 6}, 109 | DisabledChannels: []int{2, 3, 4, 5, 6}, 110 | ExpectedUplinkChannels: []int{0, 1}, 111 | ExpectedLinkADRReqPayloads: []lorawan.LinkADRReqPayload{ 112 | { 113 | ChMask: lorawan.ChMask{true, true}, 114 | }, 115 | }, 116 | // we disable the CFList channels as they became inactive 117 | }, 118 | } 119 | 120 | for i, test := range tests { 121 | Convey(fmt.Sprintf("testing %s [%d]", test.Name, i), func() { 122 | for _, c := range test.DisabledChannels { 123 | So(band.DisableUplinkChannelIndex(c), ShouldBeNil) 124 | } 125 | pls := band.GetLinkADRReqPayloadsForEnabledUplinkChannelIndices(test.NodeChannels) 126 | So(pls, ShouldResemble, test.ExpectedLinkADRReqPayloads) 127 | 128 | chans, err := band.GetEnabledUplinkChannelIndicesForLinkADRReqPayloads(test.NodeChannels, pls) 129 | So(err, ShouldBeNil) 130 | So(chans, ShouldResemble, test.ExpectedUplinkChannels) 131 | }) 132 | } 133 | 134 | }) 135 | 136 | Convey("Then GetChannel takes the extra channels into consideration", func() { 137 | tests := []uint32{ 138 | 868900000, 139 | 869100000, 140 | 864100000, 141 | 864300000, 142 | 864500000, 143 | 864700000, 144 | 864900000, 145 | } 146 | 147 | for expChannel, expFreq := range tests { 148 | var defaultChannel bool 149 | if expChannel < 2 { 150 | defaultChannel = true 151 | } 152 | channel, err := band.GetUplinkChannelIndex(expFreq, defaultChannel) 153 | So(err, ShouldBeNil) 154 | So(channel, ShouldEqual, expChannel) 155 | } 156 | }) 157 | 158 | Convey("Then GetCFList returns the expected CFList", func() { 159 | cFList := band.GetCFList(LoRaWAN_1_0_2) 160 | So(cFList, ShouldNotBeNil) 161 | So(cFList, ShouldResemble, &lorawan.CFList{ 162 | CFListType: lorawan.CFListChannel, 163 | Payload: &lorawan.CFListChannelPayload{ 164 | Channels: [5]uint32{ 165 | 864100000, 166 | 864300000, 167 | 864500000, 168 | 864700000, 169 | 864900000, 170 | }, 171 | }, 172 | }) 173 | }) 174 | }) 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /band/errors.go: -------------------------------------------------------------------------------- 1 | package band 2 | 3 | import "errors" 4 | 5 | // errors 6 | var ( 7 | ErrChannelDoesNotExist = errors.New("lorawan/band: channel does not exist") 8 | ) 9 | -------------------------------------------------------------------------------- /cid_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=CID"; DO NOT EDIT. 2 | 3 | package lorawan 4 | 5 | import "strconv" 6 | 7 | const ( 8 | _CID_name_0 = "ResetIndLinkCheckReqLinkADRReqDutyCycleReqRXParamSetupReqDevStatusReqNewChannelReqRXTimingSetupReqTXParamSetupReqDLChannelReqRekeyIndADRParamSetupReqDeviceTimeReqForceRejoinReqRejoinParamSetupReqPingSlotInfoReqPingSlotChannelReq" 9 | _CID_name_1 = "BeaconFreqReq" 10 | _CID_name_2 = "DeviceModeInd" 11 | ) 12 | 13 | var ( 14 | _CID_index_0 = [...]uint8{0, 8, 20, 30, 42, 57, 69, 82, 98, 113, 125, 133, 149, 162, 176, 195, 210, 228} 15 | ) 16 | 17 | func (i CID) String() string { 18 | switch { 19 | case 1 <= i && i <= 17: 20 | i -= 1 21 | return _CID_name_0[_CID_index_0[i]:_CID_index_0[i+1]] 22 | case i == 19: 23 | return _CID_name_1 24 | case i == 32: 25 | return _CID_name_2 26 | default: 27 | return "CID(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devicemodeclass_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=DeviceModeClass"; DO NOT EDIT. 2 | 3 | package lorawan 4 | 5 | import "strconv" 6 | 7 | const _DeviceModeClass_name = "DeviceModeClassADeviceModeRFUDeviceModeClassC" 8 | 9 | var _DeviceModeClass_index = [...]uint8{0, 16, 29, 45} 10 | 11 | func (i DeviceModeClass) String() string { 12 | if i >= DeviceModeClass(len(_DeviceModeClass_index)-1) { 13 | return "DeviceModeClass(" + strconv.FormatInt(int64(i), 10) + ")" 14 | } 15 | return _DeviceModeClass_name[_DeviceModeClass_index[i]:_DeviceModeClass_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package lorawan provides structures and tools to read and write LoRaWAN 1.0 and 1.1 frames from and to a slice of bytes. 2 | package lorawan 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | lorawan: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-devel 7 | volumes: 8 | - ./:/lorawan 9 | links: 10 | - redis 11 | redis: 12 | image: redis:6-alpine 13 | -------------------------------------------------------------------------------- /eirp.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import "errors" 4 | 5 | var eirpTable = [...]float32{ 6 | 8, // 0 7 | 10, // 1 8 | 12, // 2 9 | 13, // 3 10 | 14, // 4 11 | 16, // 5 12 | 18, // 6 13 | 20, // 7 14 | 21, // 8 15 | 24, // 9 16 | 26, // 10 17 | 27, // 11 18 | 29, // 12 19 | 30, // 13 20 | 33, // 14 21 | 36, // 15 22 | } 23 | 24 | // GetTXParamSetupEIRPIndex returns the coded value for the given EIRP (dBm). 25 | // Note that it returns the coded value that is closest to the given EIRP, 26 | // without exceeding it. 27 | func GetTXParamSetupEIRPIndex(eirp float32) uint8 { 28 | var out uint8 29 | for i, e := range eirpTable { 30 | if e > eirp { 31 | return out 32 | } 33 | out = uint8(i) 34 | } 35 | return out 36 | } 37 | 38 | // GetTXParamsetupEIRP returns the EIRP (dBm) for the coded value. 39 | func GetTXParamSetupEIRP(index uint8) (float32, error) { 40 | if int(index) > len(eirpTable)-1 { 41 | return 0, errors.New("lorawan: invalid eirp index") 42 | } 43 | return eirpTable[index], nil 44 | } 45 | -------------------------------------------------------------------------------- /eirp_test.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetTXParamSetupEIRPIndex(t *testing.T) { 11 | assert := require.New(t) 12 | 13 | tests := []struct { 14 | EIRP float32 15 | ExpectedIndex uint8 16 | }{ 17 | {8, 0}, 18 | {9, 0}, 19 | {10, 1}, 20 | {36, 15}, 21 | {37, 15}, 22 | {12.15, 2}, 23 | } 24 | 25 | for _, tst := range tests { 26 | assert.Equal(tst.ExpectedIndex, GetTXParamSetupEIRPIndex(tst.EIRP)) 27 | } 28 | } 29 | 30 | func TestGetTXParamSetupEIRP(t *testing.T) { 31 | assert := require.New(t) 32 | 33 | tests := []struct { 34 | Index uint8 35 | EIRP float32 36 | Error error 37 | }{ 38 | {0, 8, nil}, 39 | {15, 36, nil}, 40 | {16, 0, errors.New("lorawan: invalid eirp index")}, 41 | } 42 | 43 | for _, tst := range tests { 44 | e, err := GetTXParamSetupEIRP(tst.Index) 45 | assert.Equal(tst.Error, err) 46 | 47 | if err == nil { 48 | assert.Equal(tst.EIRP, e) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /fhdr.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // DevAddr represents the device address. 13 | type DevAddr [4]byte 14 | 15 | // NetIDType returns the NetID type of the DevAddr. 16 | func (a DevAddr) NetIDType() int { 17 | for i := 7; i >= 0; i-- { 18 | if a[0]&(1<> uint32(prefixLength) // shift back for the prefix MSB 105 | devAddr |= nwkID 106 | 107 | binary.BigEndian.PutUint32(a[:], devAddr) 108 | } 109 | 110 | func (a DevAddr) getNwkID(prefixLength, nwkIDBits int) []byte { 111 | // convert DevAddr to uint32 112 | temp := binary.BigEndian.Uint32(a[:]) 113 | 114 | // clear prefix 115 | temp = temp << uint32(prefixLength) 116 | 117 | // shift to starting of NwkID 118 | temp = temp >> (32 - uint32(nwkIDBits)) 119 | 120 | // back to bytes 121 | out := make([]byte, 4) 122 | binary.BigEndian.PutUint32(out, temp) 123 | 124 | bLen := nwkIDBits / 8 125 | if nwkIDBits%8 != 0 { 126 | bLen++ 127 | } 128 | 129 | return out[len(out)-bLen:] 130 | } 131 | 132 | // MarshalBinary marshals the object in binary form. 133 | func (a DevAddr) MarshalBinary() ([]byte, error) { 134 | out := make([]byte, len(a)) 135 | for i, v := range a { 136 | // little endian 137 | out[len(a)-i-1] = v 138 | } 139 | return out, nil 140 | } 141 | 142 | // UnmarshalBinary decodes the object from binary form. 143 | func (a *DevAddr) UnmarshalBinary(data []byte) error { 144 | if len(data) != len(a) { 145 | return fmt.Errorf("lorawan: %d bytes of data are expected", len(a)) 146 | } 147 | for i, v := range data { 148 | // little endian 149 | a[len(a)-i-1] = v 150 | } 151 | return nil 152 | } 153 | 154 | // MarshalText implements encoding.TextMarshaler. 155 | func (a DevAddr) MarshalText() ([]byte, error) { 156 | return []byte(a.String()), nil 157 | } 158 | 159 | // UnmarshalText implements encoding.TextUnmarshaler. 160 | func (a *DevAddr) UnmarshalText(text []byte) error { 161 | b, err := hex.DecodeString(strings.TrimPrefix(string(text), "0x")) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | if len(b) != len(a) { 167 | return fmt.Errorf("lorawan: exactly %d bytes are expected", len(a)) 168 | } 169 | copy(a[:], b) 170 | return nil 171 | } 172 | 173 | // String implements fmt.Stringer. 174 | func (a DevAddr) String() string { 175 | return hex.EncodeToString(a[:]) 176 | } 177 | 178 | // Scan implements sql.Scanner. 179 | func (a *DevAddr) Scan(src interface{}) error { 180 | b, ok := src.([]byte) 181 | if !ok { 182 | return errors.New("lorawan: []byte type expected") 183 | } 184 | if len(b) != len(a) { 185 | return fmt.Errorf("lorawan []byte must have length %d", len(a)) 186 | } 187 | copy(a[:], b) 188 | return nil 189 | } 190 | 191 | // Value implements driver.Valuer. 192 | func (a DevAddr) Value() (driver.Value, error) { 193 | return a[:], nil 194 | } 195 | 196 | // FCtrl represents the FCtrl (frame control) field. 197 | // Please note that the FPending and ClassB are mapped to the same bit. This 198 | // means that when unmarshaling from a byte-slice, both fields will contain 199 | // the same value (either true or false). 200 | type FCtrl struct { 201 | ADR bool `json:"adr"` 202 | ADRACKReq bool `json:"adrAckReq"` 203 | ACK bool `json:"ack"` 204 | FPending bool `json:"fPending"` // only used for downlink messages 205 | ClassB bool `json:"classB"` // only used for uplink messages 206 | fOptsLen uint8 // will be set automatically by the FHDR when serialized to []byte 207 | } 208 | 209 | // MarshalBinary marshals the object in binary form. 210 | func (c FCtrl) MarshalBinary() ([]byte, error) { 211 | if c.fOptsLen > 15 { 212 | return []byte{}, errors.New("lorawan: max value of FOptsLen is 15") 213 | } 214 | 215 | var b byte 216 | if c.ADR { 217 | b |= 0x80 218 | } 219 | if c.ADRACKReq { 220 | b |= 0x40 221 | } 222 | if c.ACK { 223 | b |= 0x20 224 | } 225 | if c.ClassB || c.FPending { 226 | b |= 0x10 227 | } 228 | b |= byte(c.fOptsLen) & 0x0f 229 | 230 | return []byte{b}, nil 231 | } 232 | 233 | // UnmarshalBinary decodes the object from binary form. 234 | func (c *FCtrl) UnmarshalBinary(data []byte) error { 235 | if len(data) != 1 { 236 | return errors.New("lorawan: 1 byte of data is expected") 237 | } 238 | 239 | c.ADR = data[0]&0x80 != 0 240 | c.ADRACKReq = data[0]&0x40 != 0 241 | c.ACK = data[0]&0x20 != 0 242 | c.ClassB = data[0]&0x10 != 0 243 | c.FPending = data[0]&0x10 != 0 244 | c.fOptsLen = data[0] & 0x0f 245 | 246 | return nil 247 | } 248 | 249 | // FHDR represents the frame header. 250 | type FHDR struct { 251 | DevAddr DevAddr `json:"devAddr"` 252 | FCtrl FCtrl `json:"fCtrl"` 253 | FCnt uint32 `json:"fCnt"` // only the least-significant 16 bits will be marshalled 254 | FOpts []Payload `json:"fOpts"` // max. number of allowed bytes is 15 255 | } 256 | 257 | // MarshalBinary marshals the object in binary form. 258 | func (h FHDR) MarshalBinary() ([]byte, error) { 259 | var b []byte 260 | var err error 261 | var opts []byte 262 | 263 | for _, mac := range h.FOpts { 264 | b, err = mac.MarshalBinary() 265 | if err != nil { 266 | return []byte{}, err 267 | } 268 | opts = append(opts, b...) 269 | } 270 | h.FCtrl.fOptsLen = uint8(len(opts)) 271 | if h.FCtrl.fOptsLen > 15 { 272 | return []byte{}, errors.New("lorawan: max number of FOpts bytes is 15") 273 | } 274 | 275 | out := make([]byte, 0, 7+h.FCtrl.fOptsLen) 276 | b, err = h.DevAddr.MarshalBinary() 277 | if err != nil { 278 | return []byte{}, err 279 | } 280 | out = append(out, b...) 281 | 282 | b, err = h.FCtrl.MarshalBinary() 283 | if err != nil { 284 | return []byte{}, err 285 | } 286 | out = append(out, b...) 287 | fCntBytes := make([]byte, 4) 288 | binary.LittleEndian.PutUint32(fCntBytes, h.FCnt) 289 | out = append(out, fCntBytes[0:2]...) 290 | out = append(out, opts...) 291 | 292 | return out, nil 293 | } 294 | 295 | // UnmarshalBinary decodes the object from binary form. 296 | func (h *FHDR) UnmarshalBinary(uplink bool, data []byte) error { 297 | if len(data) < 7 { 298 | return errors.New("lorawan: at least 7 bytes are expected") 299 | } 300 | 301 | if err := h.DevAddr.UnmarshalBinary(data[0:4]); err != nil { 302 | return err 303 | } 304 | if err := h.FCtrl.UnmarshalBinary(data[4:5]); err != nil { 305 | return err 306 | } 307 | fCntBytes := make([]byte, 4) 308 | copy(fCntBytes, data[5:7]) 309 | h.FCnt = binary.LittleEndian.Uint32(fCntBytes) 310 | 311 | if len(data) > 7 { 312 | h.FOpts = []Payload{ 313 | &DataPayload{Bytes: data[7:]}, 314 | } 315 | } 316 | 317 | return nil 318 | } 319 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brocaar/lorawan 2 | 3 | require ( 4 | github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 5 | github.com/go-redis/redis/v8 v8.8.3 6 | github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f // indirect 7 | github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 8 | github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect 9 | github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect 10 | github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 // indirect 11 | github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb // indirect 12 | github.com/kr/pretty v0.1.0 // indirect 13 | github.com/pkg/errors v0.9.1 14 | github.com/sirupsen/logrus v1.7.0 15 | github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect 16 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a 17 | github.com/stretchr/testify v1.7.0 18 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 19 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 20 | ) 21 | 22 | go 1.15 23 | -------------------------------------------------------------------------------- /gps/gps.go: -------------------------------------------------------------------------------- 1 | // Package gps provides functions to handle Time <> GPS Epoch time conversion. 2 | package gps 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | var gpsEpochTime = time.Date(1980, time.January, 6, 0, 0, 0, 0, time.UTC) 10 | 11 | var leapSecondsTable = []struct { 12 | Time time.Time 13 | Duration time.Duration 14 | }{ 15 | {Time: time.Date(1981, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 16 | {Time: time.Date(1982, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 17 | {Time: time.Date(1983, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 18 | {Time: time.Date(1985, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 19 | {Time: time.Date(1987, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 20 | {Time: time.Date(1989, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 21 | {Time: time.Date(1990, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 22 | {Time: time.Date(1992, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 23 | {Time: time.Date(1993, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 24 | {Time: time.Date(1994, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 25 | {Time: time.Date(1995, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 26 | {Time: time.Date(1997, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 27 | {Time: time.Date(1998, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 28 | {Time: time.Date(2005, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 29 | {Time: time.Date(2008, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 30 | {Time: time.Date(2012, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 31 | {Time: time.Date(2015, time.June, 30, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 32 | {Time: time.Date(2016, time.December, 31, 23, 59, 59, 0, time.UTC), Duration: time.Second}, 33 | } 34 | 35 | // Time represents a GPS time wrapper. 36 | type Time time.Time 37 | 38 | // NewTimeFromTimeSinceGPSEpoch returns a new Time given a time since GPS epoch 39 | // and will apply the leap second correction. 40 | func NewTimeFromTimeSinceGPSEpoch(sinceEpoch time.Duration) Time { 41 | t := gpsEpochTime.Add(sinceEpoch) 42 | for _, ls := range leapSecondsTable { 43 | if ls.Time.Before(t) { 44 | t = t.Add(-ls.Duration) 45 | } 46 | } 47 | 48 | return Time(t) 49 | } 50 | 51 | // TimeSinceGPSEpoch returns the time duration since GPS epoch, corrected with 52 | // the leap seconds. 53 | func (t Time) TimeSinceGPSEpoch() time.Duration { 54 | var offset time.Duration 55 | for _, ls := range leapSecondsTable { 56 | if ls.Time.Before(time.Time(t)) { 57 | offset += ls.Duration 58 | } 59 | } 60 | 61 | return time.Time(t).Sub(gpsEpochTime) + offset 62 | } 63 | 64 | // String implements the Stringer interface. 65 | func (t Time) String() string { 66 | return fmt.Sprintf("%s (%s since GPS epoch)", time.Time(t).String(), t.TimeSinceGPSEpoch().String()) 67 | } 68 | -------------------------------------------------------------------------------- /gps/gps_test.go: -------------------------------------------------------------------------------- 1 | package gps 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTime(t *testing.T) { 12 | 13 | tests := []struct { 14 | Time time.Time 15 | TimeSinceGPSEpoch time.Duration 16 | }{ 17 | {Time: gpsEpochTime, TimeSinceGPSEpoch: 0}, 18 | {Time: time.Date(2010, time.January, 28, 16, 36, 24, 0, time.UTC), TimeSinceGPSEpoch: 948731799 * time.Second}, 19 | {Time: time.Date(2025, time.July, 14, 0, 0, 0, 0, time.UTC), TimeSinceGPSEpoch: 1436486418 * time.Second}, 20 | {Time: time.Date(2012, time.June, 30, 23, 59, 59, 0, time.UTC), TimeSinceGPSEpoch: 1025136014 * time.Second}, 21 | {Time: time.Date(2012, time.July, 1, 0, 0, 0, 0, time.UTC), TimeSinceGPSEpoch: 1025136016 * time.Second}, 22 | } 23 | 24 | for i, test := range tests { 25 | t.Run(fmt.Sprintf("Testing: %s == %s [%d]", test.Time, test.TimeSinceGPSEpoch, i), func(t *testing.T) { 26 | assert := require.New(t) 27 | gpsTime := Time(test.Time) 28 | 29 | assert.Equal(test.TimeSinceGPSEpoch, gpsTime.TimeSinceGPSEpoch()) 30 | 31 | gpsTime = NewTimeFromTimeSinceGPSEpoch(test.TimeSinceGPSEpoch) 32 | assert.True(time.Time(gpsTime).Equal(test.Time)) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /jointype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=JoinType"; DO NOT EDIT. 2 | 3 | package lorawan 4 | 5 | import "strconv" 6 | 7 | const ( 8 | _JoinType_name_0 = "RejoinRequestType0RejoinRequestType1RejoinRequestType2" 9 | _JoinType_name_1 = "JoinRequestType" 10 | ) 11 | 12 | var ( 13 | _JoinType_index_0 = [...]uint8{0, 18, 36, 54} 14 | ) 15 | 16 | func (i JoinType) String() string { 17 | switch { 18 | case 0 <= i && i <= 2: 19 | return _JoinType_name_0[_JoinType_index_0[i]:_JoinType_index_0[i+1]] 20 | case i == 255: 21 | return _JoinType_name_1 22 | default: 23 | return "JoinType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mac_command_payload_test.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type macCommandPayloadTest struct { 12 | Payload MACCommandPayload 13 | Bytes []byte 14 | Error error 15 | } 16 | 17 | type MACCommandPayloadTestSuite struct { 18 | suite.Suite 19 | } 20 | 21 | func (ts MACCommandPayloadTestSuite) run(newPLFunc func() MACCommandPayload, tests []macCommandPayloadTest) { 22 | assert := require.New(ts.T()) 23 | 24 | for _, tst := range tests { 25 | if tst.Payload != nil { 26 | b, err := tst.Payload.MarshalBinary() 27 | assert.Equal(tst.Error, err) 28 | assert.Equal(tst.Bytes, b) 29 | 30 | // if there is a Payload and error, skip to the next test 31 | if tst.Error != nil { 32 | continue 33 | } 34 | } 35 | 36 | pl := newPLFunc() 37 | err := pl.UnmarshalBinary(tst.Bytes) 38 | if tst.Error == nil { 39 | assert.NoError(err) 40 | } else { 41 | assert.Equal(tst.Error, err) 42 | } 43 | 44 | if err == nil { 45 | assert.Equal(tst.Payload, pl) 46 | } 47 | } 48 | } 49 | 50 | func (ts MACCommandPayloadTestSuite) TestDeviceModeIndClass() { 51 | tests := []macCommandPayloadTest{ 52 | { 53 | Bytes: []byte{}, 54 | Error: errors.New("lorawan: 1 byte of data is expected"), 55 | }, 56 | { 57 | Bytes: []byte{0x00, 0x01}, 58 | Error: errors.New("lorawan: 1 byte of data is expected"), 59 | }, 60 | { 61 | Payload: &DeviceModeIndPayload{ 62 | Class: DeviceModeClassA, 63 | }, 64 | Bytes: []byte{0x00}, 65 | }, 66 | { 67 | Payload: &DeviceModeIndPayload{ 68 | Class: DeviceModeClassC, 69 | }, 70 | Bytes: []byte{0x02}, 71 | }, 72 | } 73 | 74 | ts.run(func() MACCommandPayload { return &DeviceModeIndPayload{} }, tests) 75 | } 76 | 77 | func (ts MACCommandPayloadTestSuite) TestDeviceModeConfClass() { 78 | tests := []macCommandPayloadTest{ 79 | { 80 | Bytes: []byte{}, 81 | Error: errors.New("lorawan: 1 byte of data is expected"), 82 | }, 83 | { 84 | Bytes: []byte{0x00, 0x01}, 85 | Error: errors.New("lorawan: 1 byte of data is expected"), 86 | }, 87 | { 88 | Payload: &DeviceModeConfPayload{ 89 | Class: DeviceModeClassA, 90 | }, 91 | Bytes: []byte{0x00}, 92 | }, 93 | { 94 | Payload: &DeviceModeConfPayload{ 95 | Class: DeviceModeClassC, 96 | }, 97 | Bytes: []byte{0x02}, 98 | }, 99 | } 100 | 101 | ts.run(func() MACCommandPayload { return &DeviceModeConfPayload{} }, tests) 102 | } 103 | 104 | func (ts MACCommandPayloadTestSuite) TestTXParamSetupReqPayload() { 105 | tests := []macCommandPayloadTest{ 106 | { 107 | Bytes: []byte{}, 108 | Error: errors.New("lorawan: 1 byte of data is expected"), 109 | }, 110 | { 111 | Payload: &TXParamSetupReqPayload{ 112 | UplinkDwellTime: DwellTime400ms, 113 | DownlinkDwelltime: DwellTimeNoLimit, 114 | MaxEIRP: 7, 115 | }, 116 | Bytes: []byte{0x17}, 117 | }, 118 | { 119 | Payload: &TXParamSetupReqPayload{ 120 | UplinkDwellTime: DwellTimeNoLimit, 121 | DownlinkDwelltime: DwellTime400ms, 122 | MaxEIRP: 7, 123 | }, 124 | Bytes: []byte{0x27}, 125 | }, 126 | { 127 | Payload: &TXParamSetupReqPayload{ 128 | UplinkDwellTime: DwellTimeNoLimit, 129 | DownlinkDwelltime: DwellTime400ms, 130 | MaxEIRP: 16, 131 | }, 132 | Error: errors.New("lorawan: max value of MaxEIRP is 15"), 133 | }, 134 | } 135 | 136 | ts.run(func() MACCommandPayload { return &TXParamSetupReqPayload{} }, tests) 137 | } 138 | 139 | func TestMACCommandPayloads(t *testing.T) { 140 | suite.Run(t, new(MACCommandPayloadTestSuite)) 141 | } 142 | -------------------------------------------------------------------------------- /macpayload.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // MACPayload represents the MAC payload. Use NewMACPayload for creating a new 8 | // MACPayload. 9 | type MACPayload struct { 10 | FHDR FHDR `json:"fhdr"` 11 | FPort *uint8 `json:"fPort"` // optional, but must be set when FRMPayload is set 12 | FRMPayload []Payload `json:"frmPayload"` 13 | } 14 | 15 | func (p MACPayload) marshalPayload() ([]byte, error) { 16 | var out []byte 17 | var b []byte 18 | var err error 19 | for _, fp := range p.FRMPayload { 20 | if mac, ok := fp.(*MACCommand); ok { 21 | if p.FPort == nil || (p.FPort != nil && *p.FPort != 0) { 22 | return []byte{}, errors.New("lorawan: a MAC command is only allowed when FPort=0") 23 | } 24 | b, err = mac.MarshalBinary() 25 | } else { 26 | b, err = fp.MarshalBinary() 27 | } 28 | if err != nil { 29 | return nil, err 30 | } 31 | out = append(out, b...) 32 | } 33 | return out, nil 34 | } 35 | 36 | // MarshalBinary marshals the object in binary form. 37 | func (p MACPayload) MarshalBinary() ([]byte, error) { 38 | var b []byte 39 | var out []byte 40 | var err error 41 | 42 | b, err = p.FHDR.MarshalBinary() 43 | if err != nil { 44 | return nil, err 45 | } 46 | out = append(out, b...) 47 | 48 | if p.FPort == nil { 49 | if len(p.FRMPayload) != 0 { 50 | return nil, errors.New("lorawan: FPort must be set when FRMPayload is not empty") 51 | } 52 | return out, nil 53 | } else if len(p.FHDR.FOpts) != 0 && *p.FPort == 0 { 54 | return nil, errors.New("lorawan: FPort must not be 0 when FOpts are set") 55 | } 56 | 57 | out = append(out, *p.FPort) 58 | 59 | if b, err = p.marshalPayload(); err != nil { 60 | return nil, err 61 | } 62 | out = append(out, b...) 63 | return out, nil 64 | } 65 | 66 | // UnmarshalBinary decodes the object from binary form. 67 | func (p *MACPayload) UnmarshalBinary(uplink bool, data []byte) error { 68 | dataLen := len(data) 69 | 70 | // check that there are enough bytes to decode a minimal FHDR 71 | if dataLen < 7 { 72 | return errors.New("lorawan: at least 7 bytes needed to decode FHDR") 73 | } 74 | 75 | // unmarshal FCtrl so we know the FOptsLen 76 | if err := p.FHDR.FCtrl.UnmarshalBinary(data[4:5]); err != nil { 77 | return err 78 | } 79 | 80 | // check that there are at least as many bytes as FOptsLen claims 81 | if dataLen < 7+int(p.FHDR.FCtrl.fOptsLen) { 82 | return errors.New("lorawan: not enough bytes to decode FHDR") 83 | } 84 | 85 | // decode the full FHDR (including optional FOpts) 86 | if err := p.FHDR.UnmarshalBinary(uplink, data[0:7+p.FHDR.FCtrl.fOptsLen]); err != nil { 87 | return err 88 | } 89 | 90 | // decode the optional FPort 91 | if dataLen > 7+int(p.FHDR.FCtrl.fOptsLen) { 92 | fPort := uint8(data[7+int(p.FHDR.FCtrl.fOptsLen)]) 93 | p.FPort = &fPort 94 | } 95 | 96 | // decode the rest of the payload (if present) 97 | if dataLen > 7+int(p.FHDR.FCtrl.fOptsLen)+1 { 98 | if p.FPort != nil && *p.FPort == 0 && p.FHDR.FCtrl.fOptsLen > 0 { 99 | return errors.New("lorawan: FPort must not be 0 when FOpts are set") 100 | } 101 | 102 | // even when FPort = 0, we store the mac-commands within a DataPayload. 103 | // only after decryption we're able to unmarshal them. 104 | p.FRMPayload = []Payload{&DataPayload{Bytes: data[7+p.FHDR.FCtrl.fOptsLen+1:]}} 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /macpayload_test.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "testing" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestMACPayload(t *testing.T) { 13 | Convey("Given an empty MACPayload", t, func() { 14 | var p MACPayload 15 | Convey("Then MarshalBinary returns []byte{0, 0, 0, 0, 0, 0, 0}", func() { 16 | b, err := p.MarshalBinary() 17 | So(err, ShouldBeNil) 18 | So(b, ShouldResemble, []byte{0, 0, 0, 0, 0, 0, 0}) 19 | }) 20 | 21 | Convey("Given FPort=1", func() { 22 | fPort := uint8(1) 23 | p.FPort = &fPort 24 | 25 | Convey("Given FRMPayload contains a MACCommand", func() { 26 | p.FRMPayload = []Payload{&MACCommand{CID: LinkCheckReq}} 27 | Convey("Then MarshalBinary returns an error that FPort must be 0", func() { 28 | _, err := p.MarshalBinary() 29 | So(err, ShouldResemble, errors.New("lorawan: a MAC command is only allowed when FPort=0")) 30 | }) 31 | }) 32 | }) 33 | 34 | Convey("Given FPort=nil", func() { 35 | 36 | Convey("Given FRMPayload is not empty", func() { 37 | p.FRMPayload = []Payload{&DataPayload{Bytes: []byte{1}}} 38 | Convey("Then MarshalBinary returns an error that FPort must be set", func() { 39 | _, err := p.MarshalBinary() 40 | So(err, ShouldResemble, errors.New("lorawan: FPort must be set when FRMPayload is not empty")) 41 | }) 42 | }) 43 | }) 44 | 45 | Convey("Given FPort=0", func() { 46 | fPort := uint8(0) 47 | p.FPort = &fPort 48 | 49 | Convey("Given FOpts are set", func() { 50 | p.FHDR.FOpts = []Payload{&MACCommand{CID: LinkCheckReq}} 51 | Convey("Then MarshalBinary returns an error that FPort must not be 0", func() { 52 | _, err := p.MarshalBinary() 53 | So(err, ShouldResemble, errors.New("lorawan: FPort must not be 0 when FOpts are set")) 54 | }) 55 | }) 56 | }) 57 | 58 | Convey("Given FHDR(DevAddr=[4]{1, 2, 3, 4}), FPort=1, FRMPayload=[]Payload{DataPayload(Bytes=[]byte{5, 6, 7})}", func() { 59 | p.FHDR.DevAddr = DevAddr([4]byte{1, 2, 3, 4}) 60 | fPort := uint8(1) 61 | p.FPort = &fPort 62 | p.FRMPayload = []Payload{&DataPayload{[]byte{5, 6, 7}}} 63 | 64 | Convey("Then MarshalBinary returns []byte{4, 3, 2, 1, 0, 0, 0, 1, 5, 6, 7}", func() { 65 | b, err := p.MarshalBinary() 66 | So(err, ShouldBeNil) 67 | So(b, ShouldResemble, []byte{4, 3, 2, 1, 0, 0, 0, 1, 5, 6, 7}) 68 | }) 69 | }) 70 | 71 | Convey("Given uplink=true, FHDR(DevAddr=[4]{1, 2, 3, 4}), FPort=0, FRMPayload=[]Payload{MACCommand{CID: DevStatusAns, Payload: DevStatusAnsPayload(Battery=10, Margin=20)}}", func() { 72 | p.FHDR.DevAddr = DevAddr([4]byte{1, 2, 3, 4}) 73 | fPort := uint8(0) 74 | p.FPort = &fPort 75 | p.FRMPayload = []Payload{&MACCommand{CID: DevStatusAns, Payload: &DevStatusAnsPayload{Battery: 10, Margin: 20}}} 76 | 77 | Convey("Then MarshalBinary returns []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10, 20}", func() { 78 | b, err := p.MarshalBinary() 79 | So(err, ShouldBeNil) 80 | So(b, ShouldResemble, []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10, 20}) 81 | }) 82 | }) 83 | 84 | Convey("Given the slice []byte{4, 3, 2, 1, 0, 0}", func() { 85 | b := []byte{4, 3, 2, 1, 0, 0} 86 | Convey("Then UnmarshalBinary returns an error", func() { 87 | err := p.UnmarshalBinary(true, b) 88 | So(err, ShouldResemble, errors.New("lorawan: at least 7 bytes needed to decode FHDR")) 89 | }) 90 | }) 91 | 92 | Convey("Given the slice []byte{4, 3, 2, 1, 3, 0, 0, 0, 0}", func() { 93 | b := []byte{4, 3, 2, 1, 3, 0, 0, 0, 0} 94 | Convey("Then UnmarshalBinary returns an error", func() { 95 | err := p.UnmarshalBinary(true, b) 96 | So(err, ShouldResemble, errors.New("lorawan: not enough bytes to decode FHDR")) 97 | }) 98 | }) 99 | 100 | Convey("Given uplink=true and slice []byte{4, 3, 2, 1, 1, 0, 0, 2}", func() { 101 | b := []byte{4, 3, 2, 1, 1, 0, 0, 2} 102 | Convey("Then UnmarshalBinary returns no error", func() { 103 | err := p.UnmarshalBinary(true, b) 104 | So(err, ShouldBeNil) 105 | }) 106 | }) 107 | 108 | Convey("Given uplink=true and slice []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10}", func() { 109 | b := []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10} 110 | Convey("Then UnmarshalBinary returns an error", func() { 111 | err := p.UnmarshalBinary(true, b) 112 | So(err, ShouldBeNil) 113 | 114 | // normally the mac commands are unmarshaled after decryption 115 | _, err = decodeDataPayloadToMACCommands(true, p.FRMPayload) 116 | So(err, ShouldResemble, errors.New("lorawan: not enough remaining bytes")) 117 | }) 118 | }) 119 | 120 | Convey("Given uplink=true and slice []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10, 20}", func() { 121 | b := []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10, 20} 122 | 123 | Convey("Then UnmarshalBinary does not return an error", func() { 124 | err := p.UnmarshalBinary(true, b) 125 | So(err, ShouldBeNil) 126 | 127 | Convey("Then FHDR(DevAddr=[4]byte{1, 2, 3, 4})", func() { 128 | So(p.FHDR.DevAddr, ShouldEqual, DevAddr([4]byte{1, 2, 3, 4})) 129 | }) 130 | Convey("Then FPort=0", func() { 131 | So(p.FPort, ShouldNotBeNil) 132 | So(*p.FPort, ShouldEqual, 0) 133 | }) 134 | Convey("Then FRMPayload=[]Payload{MACCommand{CID: DevStatusAns, Payload: DevStatusAnsPayload(Battery=10, Margin=20)}}", func() { 135 | // mac commands are normally unmarshaled when decrypting 136 | var err error 137 | p.FRMPayload, err = decodeDataPayloadToMACCommands(true, p.FRMPayload) 138 | So(err, ShouldBeNil) 139 | 140 | So(p.FRMPayload, ShouldHaveLength, 1) 141 | mac, ok := p.FRMPayload[0].(*MACCommand) 142 | So(ok, ShouldBeTrue) 143 | So(mac.CID, ShouldEqual, DevStatusAns) 144 | 145 | pl, ok := mac.Payload.(*DevStatusAnsPayload) 146 | So(ok, ShouldBeTrue) 147 | So(pl.Battery, ShouldEqual, 10) 148 | So(pl.Margin, ShouldEqual, 20) 149 | }) 150 | }) 151 | }) 152 | 153 | Convey("Given uplink=true and slice []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10, 20, 78, 79} (one known and some unknown data)", func() { 154 | b := []byte{4, 3, 2, 1, 0, 0, 0, 0, 6, 10, 20, 78, 79} 155 | var logBytes bytes.Buffer 156 | log.SetOutput(&logBytes) 157 | 158 | Convey("Then UnmarshalBinary does not return an error", func() { 159 | err := p.UnmarshalBinary(true, b) 160 | So(err, ShouldBeNil) 161 | 162 | // mac commands are normally unmarshaled when decrypting 163 | p.FRMPayload, err = decodeDataPayloadToMACCommands(true, p.FRMPayload) 164 | So(err, ShouldBeNil) 165 | 166 | Convey("Then FHDR(DevAddr=[4]byte{1, 2, 3, 4})", func() { 167 | So(p.FHDR.DevAddr, ShouldEqual, DevAddr([4]byte{1, 2, 3, 4})) 168 | }) 169 | Convey("Then FPort=0", func() { 170 | So(p.FPort, ShouldNotBeNil) 171 | So(*p.FPort, ShouldEqual, 0) 172 | }) 173 | Convey("Then FRMPayload=[]Payload{MACCommand{CID: DevStatusAns, Payload: DevStatusAnsPayload(Battery=10, Margin=20)}}", func() { 174 | 175 | So(p.FRMPayload, ShouldHaveLength, 3) 176 | mac, ok := p.FRMPayload[0].(*MACCommand) 177 | So(ok, ShouldBeTrue) 178 | So(mac.CID, ShouldEqual, DevStatusAns) 179 | 180 | pl, ok := mac.Payload.(*DevStatusAnsPayload) 181 | So(ok, ShouldBeTrue) 182 | So(pl.Battery, ShouldEqual, 10) 183 | So(pl.Margin, ShouldEqual, 20) 184 | 185 | // unparsable mac-data 186 | So(p.FRMPayload[1].(*MACCommand).CID, ShouldEqual, 78) 187 | So(p.FRMPayload[2].(*MACCommand).CID, ShouldEqual, 79) 188 | }) 189 | }) 190 | }) 191 | 192 | Convey("Given the slice []byte{4,3, 2, 1, 0, 0, 0, 1, 6, 10, 20}", func() { 193 | b := []byte{4, 3, 2, 1, 0, 0, 0, 1, 6, 10, 20} 194 | 195 | Convey("Then UnmarshalBinary does not return an error", func() { 196 | err := p.UnmarshalBinary(false, b) 197 | So(err, ShouldBeNil) 198 | 199 | Convey("Then FHDR(DevAddr=[4]byte{1, 2, 3, 4})", func() { 200 | So(p.FHDR.DevAddr, ShouldEqual, DevAddr([4]byte{1, 2, 3, 4})) 201 | }) 202 | Convey("Then FPort=1", func() { 203 | So(p.FPort, ShouldNotBeNil) 204 | So(*p.FPort, ShouldEqual, 1) 205 | }) 206 | Convey("Then FRMPayload=[]Payload{DataPayload([]byte{6, 10, 20})}", func() { 207 | So(p.FRMPayload, ShouldHaveLength, 1) 208 | 209 | pl, ok := p.FRMPayload[0].(*DataPayload) 210 | So(ok, ShouldBeTrue) 211 | So(pl.Bytes, ShouldResemble, []byte{6, 10, 20}) 212 | }) 213 | }) 214 | }) 215 | }) 216 | } 217 | -------------------------------------------------------------------------------- /major_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Major"; DO NOT EDIT. 2 | 3 | package lorawan 4 | 5 | import "strconv" 6 | 7 | const _Major_name = "LoRaWANR1" 8 | 9 | var _Major_index = [...]uint8{0, 9} 10 | 11 | func (i Major) String() string { 12 | if i >= Major(len(_Major_index)-1) { 13 | return "Major(" + strconv.FormatInt(int64(i), 10) + ")" 14 | } 15 | return _Major_name[_Major_index[i]:_Major_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /mtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=MType"; DO NOT EDIT. 2 | 3 | package lorawan 4 | 5 | import "strconv" 6 | 7 | const _MType_name = "JoinRequestJoinAcceptUnconfirmedDataUpUnconfirmedDataDownConfirmedDataUpConfirmedDataDownRejoinRequestProprietary" 8 | 9 | var _MType_index = [...]uint8{0, 11, 21, 38, 57, 72, 89, 102, 113} 10 | 11 | func (i MType) String() string { 12 | if i >= MType(len(_MType_index)-1) { 13 | return "MType(" + strconv.FormatInt(int64(i), 10) + ")" 14 | } 15 | return _MType_name[_MType_index[i]:_MType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /netid.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // NetID represents the NetID. 13 | type NetID [3]byte 14 | 15 | // Type returns the NetID type. 16 | func (n NetID) Type() int { 17 | return int(n[0] >> 5) 18 | } 19 | 20 | // ID returns the NetID ID part. 21 | func (n NetID) ID() []byte { 22 | switch n.Type() { 23 | case 0, 1: 24 | return n.getID(6) 25 | case 2: 26 | return n.getID(9) 27 | case 3, 4, 5, 6, 7: 28 | return n.getID(21) 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | func (n NetID) getID(bits int) []byte { 35 | // convert NetID to uint32 36 | b := make([]byte, 4) 37 | copy(b[1:], n[:]) 38 | temp := binary.BigEndian.Uint32(b) 39 | 40 | // clear prefix 41 | temp = temp << uint(32-bits) 42 | temp = temp >> uint(32-bits) 43 | 44 | binary.BigEndian.PutUint32(b, temp) 45 | 46 | bLen := bits / 8 47 | if bits%8 != 0 { 48 | bLen++ 49 | } 50 | 51 | return b[len(b)-bLen:] 52 | } 53 | 54 | // String implements fmt.Stringer. 55 | func (n NetID) String() string { 56 | return hex.EncodeToString(n[:]) 57 | } 58 | 59 | // MarshalText implements encoding.TextMarshaler. 60 | func (n NetID) MarshalText() ([]byte, error) { 61 | return []byte(n.String()), nil 62 | } 63 | 64 | // UnmarshalText implements encoding.TextUnmarshaler. 65 | func (n *NetID) UnmarshalText(text []byte) error { 66 | b, err := hex.DecodeString(strings.TrimPrefix(string(text), "0x")) 67 | if err != nil { 68 | return err 69 | } 70 | if len(b) != len(n) { 71 | return fmt.Errorf("lorawan: exactly %d bytes are expected", len(n)) 72 | } 73 | copy(n[:], b) 74 | return nil 75 | } 76 | 77 | // MarshalBinary implements encoding.BinaryMarshaler. 78 | func (n NetID) MarshalBinary() ([]byte, error) { 79 | out := make([]byte, len(n)) 80 | 81 | // little endian 82 | for i, v := range n { 83 | out[len(n)-1-i] = v 84 | } 85 | 86 | return out, nil 87 | } 88 | 89 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 90 | func (n *NetID) UnmarshalBinary(data []byte) error { 91 | if len(data) != len(n) { 92 | return fmt.Errorf("lorawan: %d bytes of data are expected", len(n)) 93 | } 94 | 95 | for i, v := range data { 96 | // little endian 97 | n[len(n)-1-i] = v 98 | } 99 | 100 | return nil 101 | } 102 | 103 | // Value implements driver.Valuer. 104 | func (n NetID) Value() (driver.Value, error) { 105 | return n[:], nil 106 | } 107 | 108 | // Scan implements sql.Scanner. 109 | func (n *NetID) Scan(src interface{}) error { 110 | b, ok := src.([]byte) 111 | if !ok { 112 | return errors.New("lorawan: []byte type expected") 113 | } 114 | if len(b) != len(n) { 115 | return fmt.Errorf("lorawan: []byte must have length %d", len(n)) 116 | } 117 | copy(n[:], b) 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /netid_test.go: -------------------------------------------------------------------------------- 1 | package lorawan 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestNetID(t *testing.T) { 11 | Convey("Given a set of tests", t, func() { 12 | tests := []struct { 13 | Name string 14 | NetID NetID 15 | Type int 16 | ID []byte 17 | Bytes []byte 18 | String string 19 | }{ 20 | { 21 | Name: "NetID type 0", 22 | NetID: NetID{0, 0, 109}, 23 | Type: 0, 24 | ID: []byte{45}, 25 | Bytes: []byte{109, 0, 0}, 26 | String: "00006d", 27 | }, 28 | { 29 | Name: "NetID type 1", 30 | NetID: NetID{32, 0, 109}, 31 | Type: 1, 32 | ID: []byte{45}, 33 | Bytes: []byte{109, 0, 32}, 34 | String: "20006d", 35 | }, 36 | { 37 | Name: "NetID type 2", 38 | NetID: NetID{64, 3, 109}, 39 | Type: 2, 40 | ID: []byte{1, 109}, 41 | Bytes: []byte{109, 3, 64}, 42 | String: "40036d", 43 | }, 44 | { 45 | Name: "NetID type 3", 46 | NetID: NetID{118, 219, 109}, 47 | Type: 3, 48 | ID: []byte{22, 219, 109}, 49 | Bytes: []byte{109, 219, 118}, 50 | String: "76db6d", 51 | }, 52 | { 53 | Name: "NetID type 4", 54 | NetID: NetID{150, 219, 109}, 55 | Type: 4, 56 | ID: []byte{22, 219, 109}, 57 | Bytes: []byte{109, 219, 150}, 58 | String: "96db6d", 59 | }, 60 | { 61 | Name: "NetID type 5", 62 | NetID: NetID{182, 219, 109}, 63 | Type: 5, 64 | ID: []byte{22, 219, 109}, 65 | Bytes: []byte{109, 219, 182}, 66 | String: "b6db6d", 67 | }, 68 | { 69 | Name: "NetID type 6", 70 | NetID: NetID{214, 219, 109}, 71 | Type: 6, 72 | ID: []byte{22, 219, 109}, 73 | Bytes: []byte{109, 219, 214}, 74 | String: "d6db6d", 75 | }, 76 | { 77 | Name: "NetID type 7", 78 | NetID: NetID{246, 219, 109}, 79 | Type: 7, 80 | ID: []byte{22, 219, 109}, 81 | Bytes: []byte{109, 219, 246}, 82 | String: "f6db6d", 83 | }, 84 | } 85 | 86 | for i, test := range tests { 87 | Convey(fmt.Sprintf("Testing: %s [%d]", test.Name, i), func() { 88 | So(test.NetID.Type(), ShouldEqual, test.Type) 89 | So(test.NetID.ID(), ShouldResemble, test.ID) 90 | 91 | b, err := test.NetID.MarshalBinary() 92 | So(err, ShouldBeNil) 93 | So(b, ShouldResemble, test.Bytes) 94 | 95 | var netID NetID 96 | So(netID.UnmarshalBinary(test.Bytes), ShouldBeNil) 97 | So(netID, ShouldEqual, test.NetID) 98 | 99 | b, err = test.NetID.MarshalText() 100 | So(err, ShouldBeNil) 101 | So(string(b), ShouldEqual, test.String) 102 | 103 | So(netID.UnmarshalText([]byte(test.String)), ShouldBeNil) 104 | So(netID, ShouldEqual, test.NetID) 105 | }) 106 | } 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /sensitivity/sensitivity.go: -------------------------------------------------------------------------------- 1 | // Package sensitivity provides functions for calculating the LoRa sensitivity. 2 | package sensitivity 3 | 4 | import ( 5 | "math" 6 | ) 7 | 8 | // CalculateSensitivity calculates the LoRa sensitivity. 9 | // The bandwidth must be given in Hz! 10 | func CalculateSensitivity(bandwidth int, noiseFigure, snr float32) float32 { 11 | // see also: http://www.techplayon.com/lora-link-budget-sensitivity-calculations-example-explained/ 12 | logBW := 10 * math.Log10(float64(bandwidth)) 13 | return float32(-174 + logBW + float64(noiseFigure+snr)) 14 | } 15 | 16 | // CalculateLinkBudget calculates the link budget. 17 | // The bandwidth must be given in Hz! 18 | func CalculateLinkBudget(bandwidth int, noiseFigure, snr, txPower float32) float32 { 19 | // see also: http://www.techplayon.com/lora-link-budget-sensitivity-calculations-example-explained/ 20 | return txPower - CalculateSensitivity(bandwidth, noiseFigure, snr) 21 | } 22 | -------------------------------------------------------------------------------- /sensitivity/sensitivity_test.go: -------------------------------------------------------------------------------- 1 | package sensitivity 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCalculateSensitivity(t *testing.T) { 10 | assert := require.New(t) 11 | s := CalculateSensitivity(125000, 6, -20) 12 | 13 | assert.Equal(-137, int(s)) 14 | } 15 | 16 | func TestCalculateLinkBudget(t *testing.T) { 17 | assert := require.New(t) 18 | 19 | lb := CalculateLinkBudget(125000, 6, -20, 17) 20 | assert.Equal(154, int(lb)) 21 | } 22 | --------------------------------------------------------------------------------