├── LICENSE ├── README.md ├── graph_as923.py ├── graph_as923_datarate.py ├── image ├── as923-toa.png ├── as923-vs-others-sf10.png ├── as923-vs-others-sf12.png ├── as923-with-arib180.png ├── as923-with-dwelltime.png ├── as923-without-dwelltime.png ├── lora-toa-125.png ├── lorawan-dr-all-50b.png ├── lorawan-dr-all.png ├── lorawan-dr-sf7-base.png └── lorawan-toa.png ├── lorawan_toa.py ├── lorawan_toa_cal.py ├── test.py └── toa.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shoichi Sakane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LoRa/LoRaWAN Time on Air calculator 2 | =================================== 3 | 4 | A calculator of the time on air (ToA) of LoRa/LoRaWAN PHY frame in Python. 5 | 6 | (28-Aug-2020) Python2 has sunset already. For Python3, toa.py has been added. Don't worry. lorawan_toa.py is not changed for backward compatibility. 7 | 8 | This script refers to the section 4.1.1.6. LoRa Packet Structure, 9 | [SX1276/77/78/79 Datasheet rev.5][http://www.semtech.com/images/datasheet/sx1276.pdf]. 10 | 11 | The default parameters of the equation is based on LoRaWAN AS923 12 | in the LoRaWAN regional parameters v1.1. 13 | 14 | ## Note 15 | 16 | The default value of Explicit Header is enable. 17 | It is guessed from the PHY frame format 18 | though there is no explit text in the LoRaWAN specification. 19 | 20 | The value of LowDataRateOptimization is set automatically 21 | when the symbol duration exceeds 16ms. 22 | Because the datasheet requires that it must be used 23 | when the symbol duration exceeds 16ms. 24 | This is the case below: 25 | 26 | - SF=12 and 11 in 125 kHz. 27 | - SF=12 in 250 kHz. 28 | 29 | You can disable this feature by the --disable-auto-ldro option. 30 | The LDRO is disabled by default if you disable the auto LDRO. 31 | If you want to enable the LDRO, you can specify the --enable-ldro option. 32 | 33 | In the downlink stream, the CRC at the tail of the PHY frame is not used. 34 | To calculate the ToA for the downlink stream, 35 | the --downlink option should be specified. 36 | 37 | ## Usage 38 | 39 | ``` 40 | usage: toa.py [-h] [--band-width NUMBER] [--disable-auto-ldro] [--enable-ldro] 41 | [--disable-eh] [--downlink] [--disable-crc] [--cr NUMBER] 42 | [--preamble NUMBER] [--duty-cycle NUMBER] [-v] [-d] 43 | SF SIZE 44 | 45 | LoRa Time on Air calculator. 46 | 47 | positional arguments: 48 | SF Spreading Factor. It should be from 7 to 12. 49 | SIZE PHY payload size in byte. Remember that PHY payload 50 | (i.e. MAC frame) consists of MHDR(1) + MAC payload + 51 | MIC(4), or MHDR(1) + FHDR(7) + FPort(1) + APP + MIC(4). 52 | For example, SIZE for Join Request is going to be 23. 53 | If the size of an application message (APP) is 12, SIZE 54 | is going to be 25. 55 | 56 | optional arguments: 57 | -h, --help show this help message and exit 58 | --band-width NUMBER bandwidth in kHz. default is 125 kHz. 59 | --disable-auto-ldro disable the auto LDRO and disable LDRO. 60 | --enable-ldro This option is available when the auto LDRO is 61 | disabled. 62 | --disable-eh disable the explicit header. 63 | --downlink disable the CRC field, which is for the LoRaWAN 64 | downlink stream. 65 | --disable-crc same effect as the --downlink option. 66 | --cr NUMBER specify the CR value. default is 1 as LoRaWAN does. 67 | --preamble NUMBER specify the preamble. default is 8 for AS923. 68 | --duty-cycle NUMBER specify the duty cycle in percentage. default is 1 %. 69 | -v enable verbose mode. 70 | -d increase debug mode. 71 | ``` 72 | 73 | ## Examples 74 | 75 | with the -v option, it shows the ToA as well as the related information. 76 | below example, it show detail information in SF 12, 64 bytes of PHY payload, 77 | 125 kHz bandwidth, preamble 8. 78 | 79 | ``` 80 | % toa.py 12 64 -v 81 | PHY payload size : 64 Bytes 82 | MAC payload size : 59 Bytes 83 | Spreading Factor : 12 84 | Band width : 125 kHz 85 | Low data rate opt. : enable 86 | Explicit header : enable 87 | CR (coding rate) : 1 (4/5) 88 | Symbol Rate : 30.518 symbol/s 89 | Symbol Time : 32.768 msec/symbol 90 | Preamble size : 8 symbols 91 | Packet symbol size : 73 symbols 92 | Preamble ToA : 401.408 msec 93 | Payload ToA : 2392.064 msec 94 | Time on Air : 2793.472 msec 95 | Duty Cycle : 1 % 96 | Min span of a cycle : 279.347 sec 97 | Max Frames per day : 309 frames 98 | ``` 99 | 100 | without the -v option, it simply shows the ToA. 101 | 102 | ``` 103 | % toa.py 12 64 104 | 2793.472 105 | ``` 106 | 107 | There is an example of Semtech LoRa Caluculator Interface in the datasheet. 108 | If the parameters in that picture apply to toa.py, below is the command line. 109 | 110 | ``` 111 | % toa.py 12 8 --band-width 500 --cr 1 --disable-auto-ldro --preamble 6 --disable-eh -v 112 | PHY payload size : 8 Bytes 113 | MAC payload size : 3 Bytes 114 | Spreading Factor : 12 115 | Band width : 500 kHz 116 | Low data rate opt. : disable 117 | Explicit header : disable 118 | CR (coding rate) : 1 (4/5) 119 | Symbol Rate : 122.070 symbol/s 120 | Symbol Time : 8.192 msec/symbol 121 | Preamble size : 6 symbols 122 | Packet symbol size : 13 symbols 123 | Preamble ToA : 83.968 msec 124 | Payload ToA : 106.496 msec 125 | Time on Air : 190.464 msec 126 | RAW data rate : 1171.875 bps 127 | Duty Cycle : 1 % 128 | Min span of a cycle : 19.046 sec 129 | Max Frames per day : 4536 frames 130 | ``` 131 | 132 | 133 | ## graph_as923.py 134 | 135 | It makes a set of figures about Time on Air and PHYPayload size, 136 | especially LoRaWAN AS923 using matlib like below. 137 | 138 | ![LoRa ToA](image/as923-toa.png) 139 | 140 | -------------------------------------------------------------------------------- /graph_as923.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import matplotlib.pyplot as plt 8 | from lorawan_toa import * 9 | 10 | def get_line(list_size, n_sf, bw=125): 11 | return [ get_toa(i, n_sf, n_bw=bw)["t_packet"] for i in list_size ] 12 | 13 | ######### 14 | # 15 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 16 | ax = fig.add_subplot(1,1,1) 17 | ax.set_title("SF and ToA (BW=125 kHz)") 18 | 19 | x = range(0, 255) 20 | ax.plot(x, get_line(x, 12), "b-", label="SF12", linewidth=3, alpha=1) 21 | ax.plot(x, get_line(x, 11), "g-", label="SF11", linewidth=3, alpha=1) 22 | ax.plot(x, get_line(x, 10), "k-", label="SF10", linewidth=3, alpha=1) 23 | ax.plot(x, get_line(x, 9), "c-", label="SF9", linewidth=3, alpha=1) 24 | ax.plot(x, get_line(x, 8), "m-", label="SF8", linewidth=3, alpha=1) 25 | ax.plot(x, get_line(x, 7), "y-", label="SF7", linewidth=3, alpha=1) 26 | 27 | ax.set_xlim(0, 260) 28 | ax.set_ylim(0, 5000) 29 | ax.set_xlabel("PHY Payload Size (Byte)") 30 | ax.set_ylabel("Time on Air (ms)") 31 | ax.grid(True) 32 | ax.legend(loc="upper left", fancybox=True, shadow=True) 33 | 34 | fig.tight_layout() 35 | plt.show() 36 | fig.savefig("image/lora-toa-125.png") 37 | 38 | ######### 39 | # 40 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 41 | ax = fig.add_subplot(1,1,1) 42 | ax.set_title("AS923 No DwellTime") 43 | 44 | x = range(0, 255) 45 | ax.plot(x, get_line(x, 12), "b-", linewidth=3, alpha=0.05) 46 | ax.plot(x, get_line(x, 11), "g-", linewidth=3, alpha=0.05) 47 | ax.plot(x, get_line(x, 10), "k-", linewidth=3, alpha=0.05) 48 | ax.plot(x, get_line(x, 9), "c-", linewidth=3, alpha=0.05) 49 | ax.plot(x, get_line(x, 8), "m-", linewidth=3, alpha=0.05) 50 | ax.plot(x, get_line(x, 7), "y-", linewidth=3, alpha=0.05) 51 | 52 | # no dwellTime consideration 53 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 12), "b-", label="SF12", 54 | linewidth=3, alpha=1) 55 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 11), "g-", label="SF11", 56 | linewidth=3, alpha=1) 57 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 10), "k-", label="SF10", 58 | linewidth=3, alpha=1) 59 | ax.plot(mpsrange(8, 123), get_line(mpsrange(8, 123), 9), "c-", label="SF9", 60 | linewidth=3, alpha=1) 61 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 8), "m-", label="SF8", 62 | linewidth=3, alpha=1) 63 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", label="SF7", 64 | linewidth=3, alpha=1) 65 | 66 | ax.set_xlim(0, 260) 67 | ax.set_ylim(0, 5000) 68 | ax.set_xlabel("PHY Payload Size (Byte)") 69 | ax.set_ylabel("Time on Air (ms)") 70 | ax.grid(True) 71 | ax.legend(loc="upper left", fancybox=True, shadow=True) 72 | 73 | fig.tight_layout() 74 | plt.show() 75 | fig.savefig("image/as923-without-dwelltime.png") 76 | 77 | ######### 78 | # 79 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 80 | ax = fig.add_subplot(1,1,1) 81 | ax.set_title("AS923 DwellTime 400ms") 82 | 83 | x = range(0, 255) 84 | ax.plot(x, get_line(x, 12), "b-", linewidth=3, alpha=0.05) 85 | ax.plot(x, get_line(x, 11), "g-", linewidth=3, alpha=0.05) 86 | ax.plot(x, get_line(x, 10), "k-", linewidth=3, alpha=0.05) 87 | ax.plot(x, get_line(x, 9), "c-", linewidth=3, alpha=0.05) 88 | ax.plot(x, get_line(x, 8), "m-", linewidth=3, alpha=0.05) 89 | ax.plot(x, get_line(x, 7), "y-", linewidth=3, alpha=0.05) 90 | 91 | # required dwellTime consideration 92 | ax.plot([0], [0], "b-", label="SF12", linewidth=3, alpha=1) 93 | ax.plot([0], [0], "c-", label="SF11", linewidth=3, alpha=1) 94 | ax.plot(mpsrange(8, 19), get_line(mpsrange(8, 19), 10), "k-", label="SF10", linewidth=3, alpha=1) 95 | ax.plot(mpsrange(8, 61), get_line(mpsrange(8, 61), 9), "c-", label="SF9", linewidth=3, alpha=1) 96 | ax.plot(mpsrange(8, 133), get_line(mpsrange(8, 133), 8), "m-", label="SF8", linewidth=3, alpha=1) 97 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", label="SF7", linewidth=3, alpha=1) 98 | 99 | ax.plot(x, [400 for i in range(0, 255)], "r,", linewidth=1, alpha=0.7) 100 | 101 | ax.set_xlim(0, 260) 102 | ax.set_ylim(0, 5000) 103 | ax.set_xlabel("PHY Payload Size (Byte)") 104 | ax.set_ylabel("Time on Air (ms)") 105 | ax.grid(True) 106 | ax.legend(loc="upper left", fancybox=True, shadow=True) 107 | 108 | fig.tight_layout() 109 | plt.show() 110 | fig.savefig("image/as923-with-dwelltime.png") 111 | 112 | ######### 113 | # 114 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 115 | ax = fig.add_subplot(1,1,1) 116 | ax.set_title("AS923") 117 | 118 | x = range(0, 255) 119 | ax.plot(x, get_line(x, 12), "b-", linewidth=3, alpha=0.05) 120 | ax.plot(x, get_line(x, 11), "g-", linewidth=3, alpha=0.05) 121 | ax.plot(x, get_line(x, 10), "k-", linewidth=3, alpha=0.05) 122 | ax.plot(x, get_line(x, 9), "c-", linewidth=3, alpha=0.05) 123 | ax.plot(x, get_line(x, 8), "m-", linewidth=3, alpha=0.05) 124 | ax.plot(x, get_line(x, 7), "y-", linewidth=3, alpha=0.05) 125 | 126 | # no dwellTime consideration 127 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 12), "b-", linewidth=1.2, alpha=0.7) 128 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 11), "g-", linewidth=1.2, alpha=0.7) 129 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 10), "k-", linewidth=1.2, alpha=0.7) 130 | ax.plot(mpsrange(8, 123), get_line(mpsrange(8, 123), 9), "c-", linewidth=1.2, alpha=0.7) 131 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 8), "m-", linewidth=1.2, alpha=0.7) 132 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", linewidth=1.2, alpha=0.7) 133 | 134 | # required dwellTime consideration 135 | ax.plot([0], [0], "b-", label="SF12/125kHz", linewidth=3, alpha=1) 136 | ax.plot([0], [0], "g-", label="SF11/125kHz", linewidth=3, alpha=1) 137 | ax.plot(mpsrange(8, 19), get_line(mpsrange(8, 19), 10), "k-", 138 | label="SF10/125kHz", linewidth=3, alpha=1) 139 | ax.plot(mpsrange(8, 61), get_line(mpsrange(8, 61), 9), "c-", 140 | label="SF9/125kHz", linewidth=3, alpha=1) 141 | ax.plot(mpsrange(8, 133), get_line(mpsrange(8, 133), 8), "m-", 142 | label="SF8/125kHz", linewidth=3, alpha=1) 143 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", 144 | label="SF7/125kHz", linewidth=3, alpha=1) 145 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7, bw=250), "b--", 146 | label="SF7/250kHz", linewidth=3, alpha=0.5) 147 | 148 | ax.set_xlim(0, 260) 149 | ax.set_ylim(0, 5000) 150 | ax.set_xlabel("PHY Payload Size (Byte)") 151 | ax.set_ylabel("Time on Air (ms)") 152 | ax.grid(True) 153 | ax.legend(loc="upper left", fancybox=True, shadow=True) 154 | 155 | fig.tight_layout() 156 | plt.show() 157 | fig.savefig("image/as923-toa.png") 158 | 159 | ######### 160 | # 161 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 162 | ax = fig.add_subplot(1,1,1) 163 | ax.set_title("AS923 and ARIB STD-T108") 164 | 165 | x = range(0, 255) 166 | ax.plot(x, get_line(x, 12), "b-", linewidth=3, alpha=0.05) 167 | ax.plot(x, get_line(x, 11), "g-", linewidth=3, alpha=0.05) 168 | ax.plot(x, get_line(x, 10), "k-", linewidth=3, alpha=0.05) 169 | ax.plot(x, get_line(x, 9), "c-", linewidth=3, alpha=0.05) 170 | ax.plot(x, get_line(x, 8), "m-", linewidth=3, alpha=0.05) 171 | ax.plot(x, get_line(x, 7), "y-", linewidth=3, alpha=0.05) 172 | 173 | # no dwellTime consideration 174 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 12), "b-", linewidth=1.2, alpha=0.7) 175 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 11), "g-", linewidth=1.2, alpha=0.7) 176 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 10), "k-", linewidth=1.2, alpha=0.7) 177 | ax.plot(mpsrange(8, 123), get_line(mpsrange(8, 123), 9), "c-", linewidth=1.2, alpha=0.7) 178 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 8), "m-", linewidth=1.2, alpha=0.7) 179 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", linewidth=1.2, alpha=0.7) 180 | 181 | # required dwellTime consideration 182 | ax.plot([0], [0], "b-", label="SF12/125kHz", linewidth=3, alpha=1) 183 | ax.plot([0], [0], "g-", label="SF11/125kHz", linewidth=3, alpha=1) 184 | ax.plot(mpsrange(8, 19), get_line(mpsrange(8, 19), 10), "k-", 185 | label="SF10/125kHz", linewidth=3, alpha=1) 186 | ax.plot(mpsrange(8, 61), get_line(mpsrange(8, 61), 9), "c-", 187 | label="SF9/125kHz", linewidth=3, alpha=1) 188 | ax.plot(mpsrange(8, 133), get_line(mpsrange(8, 133), 8), "m-", 189 | label="SF8/125kHz", linewidth=3, alpha=1) 190 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", 191 | label="SF7/125kHz", linewidth=3, alpha=1) 192 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7, bw=250), "b--", 193 | label="SF7/250kHz", linewidth=3, alpha=0.5) 194 | 195 | ax.plot(x, [400 for i in range(0, 255)], "r--", linewidth=2, alpha=0.7) 196 | ax.plot(x, [200 for i in range(0, 255)], "r--", linewidth=2, alpha=0.7) 197 | ax.plot(x, [4000 for i in range(0, 255)], "r--", linewidth=2, alpha=0.7) 198 | 199 | ax.set_xlim(0, 260) 200 | ax.set_ylim(0, 5000) 201 | ax.set_xlabel("PHY Payload Size (Byte)") 202 | ax.set_ylabel("Time on Air (ms)") 203 | ax.grid(True) 204 | ax.legend(loc="upper left", fancybox=True, shadow=True) 205 | 206 | fig.tight_layout() 207 | plt.show() 208 | fig.savefig("image/as923-with-arib180.png") 209 | 210 | ######### 211 | # 212 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 213 | ax = fig.add_subplot(1,1,1) 214 | ax.set_title("AS923 vs Others (SF12)") 215 | 216 | x = range(0, 255) 217 | ax.plot(x, get_line(x, 12), "b-", linewidth=3, alpha=0.05) 218 | ax.plot(x, get_line(x, 12, bw=500), "r-", linewidth=3, alpha=0.05) 219 | 220 | # no dwellTime consideration 221 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 12), "b-", 222 | label="SF12/125kHz", linewidth=3.0, alpha=1) 223 | 224 | # LoRa: SF12 / 500 kHz 225 | ax.plot(mpsrange(8, 61), get_line(mpsrange(8, 61), 12, bw=500), "r-", 226 | label="SF12/500kHz", linewidth=3, alpha=1) 227 | 228 | ax.set_xlim(0, 260) 229 | ax.set_ylim(0, 5000) 230 | ax.set_xlabel("PHY Payload Size (Byte)") 231 | ax.set_ylabel("Time on Air (ms)") 232 | ax.grid(True) 233 | ax.legend(loc="best", fancybox=True, shadow=True) 234 | 235 | fig.tight_layout() 236 | plt.show() 237 | fig.savefig("image/as923-vs-others-sf12.png") 238 | 239 | ######### 240 | # 241 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 242 | ax = fig.add_subplot(1,1,1) 243 | ax.set_title("AS923 vs Others (SF10)") 244 | 245 | x = range(0, 255) 246 | ax.plot(x, get_line(x, 10), "b-", linewidth=3, alpha=0.05) 247 | ax.plot(x, get_line(x, 10, bw=500), "r-", linewidth=3, alpha=0.05) 248 | 249 | # no dwellTime consideration 250 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 10), "b-", 251 | label="SF10/125kHz", linewidth=3.0, alpha=1) 252 | 253 | # LoRa: SF10 / 500 kHz 254 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 10, bw=500), "r-", 255 | label="SF10/500kHz", linewidth=3, alpha=1) 256 | 257 | ax.set_xlim(0, 260) 258 | ax.set_ylim(0, 5000) 259 | ax.set_xlabel("PHY Payload Size (Byte)") 260 | ax.set_ylabel("Time on Air (ms)") 261 | ax.grid(True) 262 | ax.legend(loc="best", fancybox=True, shadow=True) 263 | 264 | fig.tight_layout() 265 | plt.show() 266 | fig.savefig("image/as923-vs-others-sf10.png") 267 | 268 | ######### 269 | # 270 | fig = plt.figure(num=None, figsize=(16, 8), facecolor='w', edgecolor='k') 271 | ax = fig.add_subplot(1,1,1) 272 | ax.set_title("LoRaWAN") 273 | 274 | x = range(0, 255) 275 | ax.plot(x, get_line(x, 12), "b-", linewidth=3, alpha=0.05) 276 | ax.plot(x, get_line(x, 11), "g-", linewidth=3, alpha=0.05) 277 | ax.plot(x, get_line(x, 10), "k-", linewidth=3, alpha=0.05) 278 | ax.plot(x, get_line(x, 9), "c-", linewidth=3, alpha=0.05) 279 | ax.plot(x, get_line(x, 8), "m-", linewidth=3, alpha=0.05) 280 | ax.plot(x, get_line(x, 7), "y-", linewidth=3, alpha=0.05) 281 | 282 | # SF BW bit rate Max. MACPayload 283 | # 12 125 250 59 284 | # 11 125 440 59 285 | # 10 125 980 59 286 | # 9 125 1760 123 287 | # 8 125 3125 250 288 | # 7 125 5470 250 289 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 12), "b-", 290 | label="SF12/125kHz", linewidth=2.0) 291 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 11), "g-", 292 | label="SF11/125kHz", linewidth=2.0) 293 | ax.plot(mpsrange(8, 59), get_line(mpsrange(8, 59), 10), "k-", 294 | label="SF10/125kHz", linewidth=2.0) 295 | ax.plot(mpsrange(8, 123), get_line(mpsrange(8, 123), 9), "c-", 296 | label="SF9/125kHz", linewidth=2.0) 297 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 8), "m-", 298 | label="SF8/125kHz", linewidth=2.0) 299 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7), "y-", 300 | label="SF7/125kHz", linewidth=2.0) 301 | 302 | # SF BW bit rate Max. MACPayload 303 | # 7 250 11000 250 304 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7, bw=250), "b-.", 305 | label="SF7/250kHz", linewidth=2.0) 306 | 307 | # SF BW bit rate Max. MACPayload 308 | # 12 500 980 61 309 | # 11 500 1760 137 310 | # 10 500 3900 250 311 | # 9 500 7000 250 312 | # 8 500 12500 250 313 | # 7 500 21900 250 314 | ax.plot(mpsrange(8, 61), get_line(mpsrange(8, 61), 12, bw=500), "b--", 315 | label="SF12/500kHz", linewidth=2.0) 316 | ax.plot(mpsrange(8, 137), get_line(mpsrange(8, 137), 11, bw=500), "g--", 317 | label="SF11/500kHz", linewidth=2.0) 318 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 10, bw=500), "k--", 319 | label="SF10/500kHz", linewidth=2.0) 320 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 9, bw=500), "c--", 321 | label="SF9/500kHz", linewidth=2.0) 322 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 8, bw=500), "m--", 323 | label="SF8/500kHz", linewidth=2.0) 324 | ax.plot(mpsrange(8, 250), get_line(mpsrange(8, 250), 7, bw=500), "y--", 325 | label="SF7/500kHz", linewidth=2.0) 326 | 327 | ax.set_xlim(0, 260) 328 | ax.set_ylim(0, 5000) 329 | ax.set_xlabel("PHY Payload Size (Byte)") 330 | ax.set_ylabel("Time on Air (ms)") 331 | ax.grid(True) 332 | ax.legend(loc="upper right", fancybox=True, shadow=True) 333 | 334 | fig.tight_layout() 335 | plt.show() 336 | fig.savefig("image/lorawan-toa.png") 337 | 338 | -------------------------------------------------------------------------------- /graph_as923_datarate.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | # -*- coding: utf-8 -*- 4 | 5 | from __future__ import print_function 6 | 7 | import sys 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | from lorawan_toa import * 11 | 12 | #### 13 | 14 | def get_y_toa(data_size, n_sf, n_bw=125): 15 | if type(data_size) == list: 16 | return [ get_y_toa(i, n_sf, n_bw=n_bw) for i in data_size ] 17 | else: 18 | return get_toa(data_size, n_sf, n_bw=n_bw)["t_packet"] 19 | 20 | def get_y_br(data_size, n_sf, n_bw=125): 21 | return [ (i*8)/(get_toa(i, n_sf, n_bw=n_bw)["t_packet"]/1000.) 22 | for i in data_size ] 23 | 24 | def get_y_br1(data_size, n_sf, n_bw=125, 25 | enable_auto_ldro=True, enable_ldro=False): 26 | if type(data_size) == list: 27 | ret = [] 28 | for i in data_size: 29 | ret.append(get_y_br1(i, n_sf, n_bw=n_bw, 30 | enable_auto_ldro=enable_auto_ldro, 31 | enable_ldro=enable_ldro)) 32 | return ret 33 | else: 34 | toa0 = get_toa(0, n_sf, n_bw=n_bw, 35 | enable_auto_ldro=enable_auto_ldro, 36 | enable_ldro=enable_ldro)["t_packet"] 37 | toa = get_toa(data_size, n_sf, n_bw=n_bw, 38 | enable_auto_ldro=enable_auto_ldro, 39 | enable_ldro=enable_ldro)["t_packet"] 40 | if toa == toa0: 41 | return 0 42 | else: 43 | return (data_size*8)/((toa - toa0)/1000.) 44 | 45 | ######## 46 | # 47 | x_nb_bytes = range(0, 40) 48 | 49 | fig = plt.figure(facecolor='w', edgecolor='k') 50 | ax = fig.add_subplot(1,1,1) 51 | ax.set_title("LoRa Data Rate (BW=125kHz, AS923)") 52 | ax.set_xlabel("PHY payload size (B)") 53 | ax.set_ylabel("Bitrate (bps)") 54 | ax.set_xlim(0, 40) 55 | #ax.set_ylim(0, 700) 56 | #ax2.set_ylim(0, 7000) 57 | 58 | lines = [] 59 | 60 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 12), "b-", label="SF12 DE=1") 61 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 11), "g-", label="SF11 DE=1") 62 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 10), "k-", label="SF10 DE=0") 63 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 9), "c-", label="SF 9 DE=0") 64 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 8), "m-", label="SF 8 DE=0") 65 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 7), "y-", label="SF 7 DE=0") 66 | 67 | #ax.plot(x_nb_bytes, [ 292.97 for i in x_nb_bytes ], "b--", lw=2, alpha=0.5) 68 | ax.plot(x_nb_bytes, [ 250.00 for i in x_nb_bytes ], "b--", lw=2, alpha=0.5) 69 | #ax.plot(x_nb_bytes, [ 537.11 for i in x_nb_bytes ], "g--", lw=2, alpha=0.5) 70 | ax.plot(x_nb_bytes, [ 440.00 for i in x_nb_bytes ], "g--", lw=2, alpha=0.5) 71 | 72 | ax.plot(x_nb_bytes, [ 976.56 for i in x_nb_bytes ], "k--", lw=2, alpha=0.5) 73 | ax.plot(x_nb_bytes, [ 1757.81 for i in x_nb_bytes ], "c--", lw=2, alpha=0.5) 74 | ax.plot(x_nb_bytes, [ 3125.00 for i in x_nb_bytes ], "m--", lw=2, alpha=0.5) 75 | ax.plot(x_nb_bytes, [ 5468.75 for i in x_nb_bytes ], "y--", lw=2, alpha=0.5) 76 | 77 | ax.axvline(12, color='k', linestyle='--', alpha=0.7) 78 | ax.scatter(16, get_y_br1(16, 7), s=80, facecolors='none', edgecolors='r') 79 | ax.scatter(19, get_y_br1(19, 7), s=80, facecolors='none', edgecolors='r') 80 | ax.scatter(26, get_y_br1(26, 7), s=80, facecolors='none', edgecolors='r') 81 | 82 | ax.grid(which="both") 83 | 84 | ax.legend(lines, [i.get_label() for i in lines], 85 | loc="upper right", prop={'size': 10}) 86 | 87 | fig.tight_layout() 88 | plt.show() 89 | fig.savefig("image/lorawan-dr-all-50b.png") 90 | 91 | ######## 92 | # 93 | x_nb_bytes = range(0, 255) 94 | 95 | fig = plt.figure(facecolor='w', edgecolor='k') 96 | ax = fig.add_subplot(1,1,1) 97 | ax.set_title("LoRa Data Rate (BW=125kHz, AS923)") 98 | ax.set_xlabel("PHY payload size (B)") 99 | ax.set_ylabel("Bitrate (bps)") 100 | ax.set_xlim(0, 260) 101 | #ax.set_ylim(0, 700) 102 | #ax2.set_ylim(0, 7000) 103 | 104 | lines = [] 105 | 106 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 12), "b-", label="SF12 DE=1") 107 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 11), "g-", label="SF11 DE=1") 108 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 10), "k-", label="SF10 DE=0") 109 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 9), "c-", label="SF 9 DE=0") 110 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 8), "m-", label="SF 8 DE=0") 111 | lines += ax.plot(x_nb_bytes, get_y_br1(x_nb_bytes, 7), "y-", label="SF 7 DE=0") 112 | 113 | #ax.plot(x_nb_bytes, [ 292.97 for i in x_nb_bytes ], "b--", lw=2, alpha=0.5) 114 | ax.plot(x_nb_bytes, [ 250.00 for i in x_nb_bytes ], "b--", lw=2, alpha=0.5) 115 | #ax.plot(x_nb_bytes, [ 537.11 for i in x_nb_bytes ], "g--", lw=2, alpha=0.5) 116 | ax.plot(x_nb_bytes, [ 440.00 for i in x_nb_bytes ], "g--", lw=2, alpha=0.5) 117 | 118 | ax.plot(x_nb_bytes, [ 976.56 for i in x_nb_bytes ], "k--", lw=2, alpha=0.5) 119 | ax.plot(x_nb_bytes, [ 1757.81 for i in x_nb_bytes ], "c--", lw=2, alpha=0.5) 120 | ax.plot(x_nb_bytes, [ 3125.00 for i in x_nb_bytes ], "m--", lw=2, alpha=0.5) 121 | ax.plot(x_nb_bytes, [ 5468.75 for i in x_nb_bytes ], "y--", lw=2, alpha=0.5) 122 | 123 | ax.axvline(12, color='k', linestyle='--', alpha=0.7) 124 | 125 | ax.grid(which="both") 126 | 127 | ax.legend(lines, [i.get_label() for i in lines], 128 | loc="upper right", prop={'size': 10}) 129 | 130 | fig.tight_layout() 131 | plt.show() 132 | fig.savefig("image/lorawan-dr-all.png") 133 | 134 | ######## 135 | # 136 | x_nb_bytes = range(0, 255) 137 | 138 | fig = plt.figure(facecolor='w', edgecolor='k') 139 | ax = fig.add_subplot(1,1,1) 140 | ax.set_title("LoRa Data Rate (SF7, BW=125kHz [AS923 DR5])") 141 | ax.set_xlabel("PHY payload size (B)") 142 | ax.set_ylabel("Time on Air (ms)") 143 | ax2 = ax.twinx() 144 | ax2.set_ylabel("Bitrate (bps)") 145 | ax.set_xlim(0, 260) 146 | ax.set_ylim(0, 800) 147 | ax2.set_ylim(0, 8000) 148 | 149 | lines = [] 150 | 151 | n_sf = 7 152 | lines += ax.plot(x_nb_bytes, get_y_toa(x_nb_bytes, n_sf), "k-", label="ToA") 153 | 154 | lines += ax2.plot(x_nb_bytes, [ 5468.75 for i in x_nb_bytes ], 155 | "r-", label="Equivalent BR.", linewidth=2) 156 | 157 | lines += ax2.plot(x_nb_bytes, get_y_br(x_nb_bytes, n_sf), 158 | "b-", label="Simple BR of PHY_PL/ToA") 159 | lines += ax2.plot(x_nb_bytes, get_y_br1(x_nb_bytes, n_sf), 160 | "y-", label="BR. PHY_PL/FixedToA auto LDRO") 161 | lines += ax2.plot(x_nb_bytes, get_y_br1(x_nb_bytes, n_sf, 162 | enable_auto_ldro=False, 163 | enable_ldro=True), 164 | "c-", label="BR. PHY_PL/FixedToA DE=1") 165 | 166 | ax2.axvline(12, color='k', linestyle='--', alpha=0.7) 167 | 168 | ax.grid(which="both") 169 | 170 | ax.legend(lines, [i.get_label() for i in lines], 171 | loc="lower right", prop={'size': 10}) 172 | 173 | fig.tight_layout() 174 | plt.show() 175 | fig.savefig("image/lorawan-dr-sf7-base.png") 176 | 177 | -------------------------------------------------------------------------------- /image/as923-toa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/as923-toa.png -------------------------------------------------------------------------------- /image/as923-vs-others-sf10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/as923-vs-others-sf10.png -------------------------------------------------------------------------------- /image/as923-vs-others-sf12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/as923-vs-others-sf12.png -------------------------------------------------------------------------------- /image/as923-with-arib180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/as923-with-arib180.png -------------------------------------------------------------------------------- /image/as923-with-dwelltime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/as923-with-dwelltime.png -------------------------------------------------------------------------------- /image/as923-without-dwelltime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/as923-without-dwelltime.png -------------------------------------------------------------------------------- /image/lora-toa-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/lora-toa-125.png -------------------------------------------------------------------------------- /image/lorawan-dr-all-50b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/lorawan-dr-all-50b.png -------------------------------------------------------------------------------- /image/lorawan-dr-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/lorawan-dr-all.png -------------------------------------------------------------------------------- /image/lorawan-dr-sf7-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/lorawan-dr-sf7-base.png -------------------------------------------------------------------------------- /image/lorawan-toa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanupoo/lorawan_toa/57ff520583cd3c06c6918a2763471c1edba4dc47/image/lorawan-toa.png -------------------------------------------------------------------------------- /lorawan_toa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 4.1.1.6. LoRaTM Packet Structure, SX1276/77/78/79 Datasheet Rev.5 Aug 2016 5 | # http://www.semtech.com/images/datasheet/sx1276.pdf 6 | 7 | import math 8 | 9 | def mpsrange(a, b): 10 | ''' 11 | Mac Payload Size range. 12 | return a list of [a, b], a <= arange(a,b) <= b 13 | ''' 14 | a += 5 # MHDR + MIC 15 | b += 6 # MHDR + MIC + 1 16 | return range(a, b) 17 | 18 | def get_toa(n_size, n_sf, n_bw=125, enable_auto_ldro=True, enable_ldro=False, 19 | enable_eh=True, enable_crc=True, n_cr=1, n_preamble=8): 20 | ''' 21 | Parameters: 22 | n_size: 23 | PL in the fomula. PHY Payload size in byte (= MAC Payload + 5) 24 | n_sf: SF (12 to 7) 25 | n_bw: Bandwidth in kHz. default is 125 kHz for AS923. 26 | enable_auto_ldro 27 | flag whether the auto Low Data Rate Optimization is enabled or not. 28 | default is True. 29 | enable_ldro: 30 | if enable_auto_ldro is disabled, LDRO is disable by default, 31 | which means that DE in the fomula is going to be 0. 32 | When enable_ldro is set to True, DE is going to be 1. 33 | LoRaWAN specification does not specify the usage. 34 | SX1276 datasheet reuiqres to enable LDRO 35 | when the symbol duration exceeds 16ms. 36 | enable_eh: 37 | when enable_eh is set to False, IH in the fomula is going to be 1. 38 | default is True, which means IH is 0. 39 | LoRaWAN always enables the explicit header. 40 | enable_crc: 41 | when enable_crc is set to False, CRC in the fomula is going to be 0. 42 | The downlink stream doesn't use the CRC in the LoRaWAN spec. 43 | default is True to calculate ToA for the uplink stream. 44 | n_cr: 45 | CR in the fomula, should be from 1 to 4. 46 | Coding Rate = (n_cr/(n_cr+1)). 47 | LoRaWAN takes alway 1. 48 | n_preamble: 49 | The preamble length in bit. 50 | default is 8 in AS923. 51 | Return: 52 | dict type contains below: 53 | r_sym: symbol rate in *second* 54 | t_sym: the time on air in millisecond*. 55 | t_preamble: 56 | v_ceil: 57 | symbol_size_payload: 58 | t_payload: 59 | t_packet: the time on air in *milisecond*. 60 | ''' 61 | r_sym = (n_bw*1000.) / math.pow(2,n_sf) 62 | t_sym = 1000. / r_sym 63 | t_preamble = (n_preamble + 4.25) * t_sym 64 | # LDRO 65 | v_DE = 0 66 | if enable_auto_ldro: 67 | if t_sym > 16: 68 | v_DE = 1 69 | elif enable_ldro: 70 | v_DE = 1 71 | # IH 72 | v_IH = 0 73 | if not enable_eh: 74 | v_IH = 1 75 | # CRC 76 | v_CRC = 1 77 | if enable_crc == False: 78 | v_CRC = 0 79 | # 80 | a = 8.*n_size - 4.*n_sf + 28 + 16*v_CRC - 20.*v_IH 81 | b = 4.*(n_sf-2.*v_DE) 82 | v_ceil = a/b 83 | n_payload = 8 + max(math.ceil(a/b)*(n_cr+4), 0) 84 | t_payload = n_payload * t_sym 85 | t_packet = t_preamble+ t_payload 86 | 87 | ret = {} 88 | ret["r_sym"] = r_sym 89 | ret["t_sym"] = t_sym 90 | ret["n_preamble"] = n_preamble 91 | ret["t_preamble"] = t_preamble 92 | ret["v_DE"] = v_DE 93 | ret["v_ceil"] = v_ceil 94 | ret["n_sym_payload"] = n_payload 95 | ret["t_payload"] = t_payload 96 | ret["t_packet"] = round(t_packet, 3) 97 | 98 | return ret 99 | 100 | if __name__ == "__main__" : 101 | import sys 102 | import argparse 103 | 104 | def parse_args(): 105 | p = argparse.ArgumentParser( 106 | description="LoRa Time on Air calculator.", 107 | epilog="") 108 | p.add_argument("n_sf", metavar="SF", type=int, 109 | help="Spreading Factor. It should be from 7 to 12.") 110 | p.add_argument("n_size", metavar="SIZE", type=int, 111 | help="""PHY payload size in byte. 112 | Remember that PHY payload (i.e. MAC frame) is consist of 113 | MHDR(1) + MAC payload + MIC(4), or 114 | MHDR(1) + FHDR(7) + FPort(1) + APP + MIC(4). 115 | For example, SIZE for Join Request is going to be 23. 116 | If the size of an application message (APP) is 117 | 12, SIZE is going to be 25. """) 118 | p.add_argument("--band-width", action="store", dest="n_bw", type=int, 119 | default=125, metavar="NUMBER", 120 | help="bandwidth in kHz. default is 125 kHz.") 121 | p.add_argument("--disable-auto-ldro", action="store_false", 122 | dest="enable_auto_ldro", 123 | help="disable the auto LDRO and disable LDRO.") 124 | p.add_argument("--enable-ldro", action="store_true", dest="enable_ldro", 125 | help="This option is available when the auto LDRO is disabled.") 126 | p.add_argument("--disable-eh", action="store_false", dest="enable_eh", 127 | help="disable the explicit header.") 128 | p.add_argument("--downlink", action="store_false", dest="enable_crc", 129 | help="disable the CRC field, which is for the LoRaWAN downlink stream.") 130 | p.add_argument("--disable-crc", action="store_false", dest="enable_crc", 131 | help="same effect as the --downlink option.") 132 | p.add_argument("--cr", action="store", dest="n_cr", 133 | type=int, default=1, metavar="NUMBER", 134 | help="specify the CR value. default is 1 as LoRaWAN does.") 135 | p.add_argument("--preamble", action="store", dest="n_preamble", 136 | type=int, default=8, metavar="NUMBER", 137 | help="specify the preamble. default is 8 for AS923.") 138 | p.add_argument("--duty-cycle", action="store", dest="n_duty_cycle", 139 | type=int, default=1, metavar="NUMBER", 140 | help="specify the duty cycle in percentage. default is 1 %%.") 141 | p.add_argument("-v", action="store_true", dest="f_verbose", 142 | default=False, 143 | help="enable verbose mode.") 144 | p.add_argument("-d", action="append_const", dest="_f_debug", default=[], 145 | const=1, help="increase debug mode.") 146 | 147 | args = p.parse_args() 148 | 149 | args.v_de = False 150 | args.debug_level = len(args._f_debug) 151 | return args 152 | 153 | # 154 | # main 155 | # 156 | opt = parse_args() 157 | ret = get_toa(opt.n_size, opt.n_sf, n_bw=opt.n_bw, 158 | enable_auto_ldro=opt.enable_auto_ldro, 159 | enable_ldro=opt.enable_ldro, 160 | enable_eh=opt.enable_eh, enable_crc=opt.enable_crc, 161 | n_cr=opt.n_cr, n_preamble=opt.n_preamble) 162 | ret["phy_pl_size"] = opt.n_size 163 | ret["mac_pl_size"] = opt.n_size - 5 164 | ret["sf"] = opt.n_sf 165 | ret["bw"] = opt.n_bw 166 | ret["ldro"] = "enable" if ret["v_DE"] else "disable" 167 | ret["eh"] = "enable" if opt.enable_eh else "disable" 168 | ret["cr"] = opt.n_cr 169 | ret["preamble"] = opt.n_preamble 170 | ret["duty_cycle"] = opt.n_duty_cycle 171 | ret["t_cycle"] = (ret["t_packet"]/1000.)*(100./ret["duty_cycle"]) 172 | ret["max_packets_day"] = 86400./ret["t_cycle"] 173 | if opt.f_verbose: 174 | print "PHY payload size : %d Bytes" % ret["phy_pl_size"] 175 | print "MAC payload size : %d Bytes" % ret["mac_pl_size"] 176 | print "Spreading Factor : %d" % ret["sf"] 177 | print "Band width : %d kHz" % ret["bw"] 178 | print "Low data rate opt. : %s" % ret["ldro"] 179 | print "Explicit header : %s" % ret["eh"] 180 | print "CR (coding rate) : %d (4/%d)" % (ret["cr"], 4+ret["cr"]) 181 | print "Symbol Rate : %.3f symbol/s" % ret["r_sym"] 182 | print "Symbol Time : %.3f msec/symbol" % ret["t_sym"] 183 | print "Preamble size : %d symbols" % ret["preamble"] 184 | print "Packet symbol size : %d symbols" % ret["n_sym_payload"] 185 | print "Preamble ToA : %.3f msec" % ret["t_preamble"] 186 | print "Payload ToA : %.3f msec" % ret["t_payload"] 187 | print "Time on Air : %.3f msec" % ret["t_packet"] 188 | print "Duty Cycle : %d %%" % ret["duty_cycle"] 189 | print "Min span of a cycle : %.3f sec" % ret["t_cycle"] 190 | print "Max Frames per day : %d frames" % ret["max_packets_day"] 191 | if opt.debug_level: 192 | ret0 = get_toa(0, opt.n_sf, n_bw=opt.n_bw, 193 | enable_auto_ldro=opt.enable_auto_ldro, 194 | enable_ldro=opt.enable_ldro, 195 | enable_eh=opt.enable_eh, enable_crc=opt.enable_crc, 196 | n_cr=opt.n_cr, n_preamble=opt.n_preamble) 197 | print "PHY PL=0 ToA : %.3f msec" % (ret0["t_packet"]) 198 | # preamble=8 cr=1? payload-len=7? crc=16 (payload) payload-crc=16 199 | # =? 48 ==> 6 bytes ? 200 | t0 = (ret["t_packet"]-ret0["t_packet"])/1000. 201 | print "PHY fr.dr.(48b) 7:15: %.3f bps" % ((8.*opt.n_size+48)/t0) 202 | print "MAC frame DR : %.3f bps" % ((8.*(opt.n_size))/t0) 203 | print "before ceil(x) : %.3f" % ret["v_ceil"] 204 | else: 205 | print "%.3f" % ret["t_packet"] 206 | -------------------------------------------------------------------------------- /lorawan_toa_cal.py: -------------------------------------------------------------------------------- 1 | # 4.1.1.6. LoRaTM Packet Structure, SX1276/77/78/79 Datasheet Rev.5 Aug 2016 2 | # http://www.semtech.com/images/datasheet/sx1276.pdf 3 | 4 | import math 5 | from argparse import ArgumentParser 6 | 7 | def mpsrange(a, b): 8 | ''' 9 | Mac Payload Size range. 10 | return a list of [a, b], a <= arange(a,b) <= b 11 | ''' 12 | a += 5 # MHDR + MIC 13 | b += 6 # MHDR + MIC + 1 14 | return range(a, b) 15 | 16 | def get_toa(n_size, n_sf, n_bw=125, enable_auto_ldro=True, enable_ldro=False, 17 | enable_eh=True, enable_crc=True, n_cr=1, n_preamble=8): 18 | ''' 19 | Parameters: 20 | n_size: 21 | PL in the fomula. PHY Payload size in byte (= MAC Payload + 5) 22 | n_sf: SF (12 to 7) 23 | n_bw: Bandwidth in kHz. default is 125 kHz for AS923. 24 | enable_auto_ldro 25 | flag whether the auto Low Data Rate Optimization is enabled or not. 26 | default is True. 27 | enable_ldro: 28 | if enable_auto_ldro is disabled, LDRO is disable by default, 29 | which means that DE in the fomula is going to be 0. 30 | When enable_ldro is set to True, DE is going to be 1. 31 | LoRaWAN specification does not specify the usage. 32 | SX1276 datasheet reuiqres to enable LDRO 33 | when the symbol duration exceeds 16ms. 34 | enable_eh: 35 | when enable_eh is set to False, IH in the fomula is going to be 1. 36 | default is True, which means IH is 0. 37 | LoRaWAN always enables the explicit header. 38 | enable_crc: 39 | when enable_crc is set to False, CRC in the fomula is going to be 0. 40 | The downlink stream doesn't use the CRC in the LoRaWAN spec. 41 | default is True to calculate ToA for the uplink stream. 42 | n_cr: 43 | CR in the fomula, should be from 1 to 4. 44 | Coding Rate = (n_cr/(n_cr+1)). 45 | LoRaWAN takes alway 1. 46 | n_preamble: 47 | The preamble length in bit. 48 | default is 8 in AS923. 49 | Return: 50 | dict type contains below: 51 | r_sym: symbol rate in *second* 52 | t_sym: the time on air in millisecond*. 53 | t_preamble: 54 | v_ceil: 55 | symbol_size_payload: 56 | t_payload: 57 | t_packet: the time on air in *milisecond*. 58 | ''' 59 | r_sym = (n_bw*1000.) / math.pow(2,n_sf) 60 | t_sym = 1000. / r_sym 61 | t_preamble = (n_preamble + 4.25) * t_sym 62 | # LDRO 63 | v_DE = 0 64 | if enable_auto_ldro: 65 | if t_sym > 16: 66 | v_DE = 1 67 | elif enable_ldro: 68 | v_DE = 1 69 | # IH 70 | v_IH = 0 71 | if not enable_eh: 72 | v_IH = 1 73 | # CRC 74 | v_CRC = 1 75 | if enable_crc == False: 76 | v_CRC = 0 77 | # 78 | a = 8.*n_size - 4.*n_sf + 28 + 16*v_CRC - 20.*v_IH 79 | b = 4.*(n_sf-2.*v_DE) 80 | v_ceil = a/b 81 | n_payload = 8 + max(math.ceil(a/b)*(n_cr+4), 0) 82 | t_payload = n_payload * t_sym 83 | t_packet = t_preamble+ t_payload 84 | 85 | ret = {} 86 | ret["r_sym"] = r_sym 87 | ret["t_sym"] = t_sym 88 | ret["n_preamble"] = n_preamble 89 | ret["t_preamble"] = t_preamble 90 | ret["v_DE"] = v_DE 91 | ret["v_ceil"] = v_ceil 92 | ret["n_sym_payload"] = n_payload 93 | ret["t_payload"] = t_payload 94 | ret["t_packet"] = round(t_packet, 3) 95 | ret["phy_pl_size"] = n_size 96 | ret["mac_pl_size"] = n_size - 5 97 | ret["sf"] = n_sf 98 | ret["bw"] = n_bw 99 | ret["ldro"] = "enable" if v_DE else "disable" 100 | ret["eh"] = "enable" if enable_eh else "disable" 101 | ret["cr"] = n_cr 102 | ret["preamble"] = n_preamble 103 | ret["raw_datarate"] = n_sf * 4/(4+n_cr) * r_sym 104 | 105 | return ret 106 | 107 | def parse_args(): 108 | p = ArgumentParser(description="LoRa Time on Air calculator.") 109 | p.add_argument("sf", metavar="SF", 110 | help="Spreading Factor. It should be from 7 to 12, or FSK") 111 | p.add_argument("n_size", metavar="SIZE", type=int, 112 | help="""PHY payload size in byte. 113 | Remember that PHY payload (i.e. MAC frame) consists of 114 | MHDR(1) + MAC payload + MIC(4), or 115 | MHDR(1) + FHDR(7) + FPort(1) + APP + MIC(4). 116 | For example, SIZE for Join Request is going to be 23. 117 | If the size of an application message (APP) is 118 | 12, SIZE is going to be 25. """) 119 | p.add_argument("--band-width", action="store", dest="n_bw", type=int, 120 | default=125, metavar="NUMBER", 121 | help="bandwidth in kHz. default is 125 kHz.") 122 | p.add_argument("--disable-auto-ldro", action="store_false", 123 | dest="enable_auto_ldro", 124 | help="disable the auto LDRO and disable LDRO.") 125 | p.add_argument("--enable-ldro", action="store_true", dest="enable_ldro", 126 | help="This option is available when the auto LDRO is disabled.") 127 | p.add_argument("--disable-eh", action="store_false", dest="enable_eh", 128 | help="disable the explicit header.") 129 | p.add_argument("--downlink", action="store_false", dest="enable_crc", 130 | help="disable the CRC field, which is for the LoRaWAN downlink stream.") 131 | p.add_argument("--disable-crc", action="store_false", dest="enable_crc", 132 | help="same effect as the --downlink option.") 133 | p.add_argument("--cr", action="store", dest="n_cr", 134 | type=int, default=1, metavar="NUMBER", 135 | help="specify the CR value. default is 1 as LoRaWAN does.") 136 | p.add_argument("--preamble", action="store", dest="n_preamble", 137 | type=int, default=8, metavar="NUMBER", 138 | help="specify the preamble. default is 8 for AS923.") 139 | p.add_argument("--duty-cycle", action="store", dest="n_duty_cycle", 140 | type=int, default=1, metavar="NUMBER", 141 | help="specify the duty cycle in percentage. default is 1 %%.") 142 | p.add_argument("-v", action="store_true", dest="f_verbose", 143 | default=False, 144 | help="enable verbose mode.") 145 | p.add_argument("-d", action="append_const", dest="_f_debug", default=[], 146 | const=1, help="increase debug mode.") 147 | 148 | args = p.parse_args() 149 | 150 | if args.sf == "FSK": 151 | args.n_sf = 0 152 | else: 153 | try: 154 | args.n_sf = int(args.sf) 155 | except ValueError: 156 | raise ValueError("ERROR: SF must be a number of 'FSK', " 157 | f"but {opt.sf}") 158 | args.v_de = False 159 | args.debug_level = len(args._f_debug) 160 | return args 161 | 162 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from lorawan_toa_cal import get_toa 5 | import unittest 6 | 7 | class TestUM(unittest.TestCase): 8 | 9 | def setUp(self): 10 | pass 11 | 12 | def test_11(self): 13 | toa = get_toa(32, 12)["t_packet"] 14 | self.assertEqual( toa, 1810.432 ) 15 | 16 | def test_12(self): 17 | toa = get_toa(32, 11)["t_packet"] 18 | self.assertEqual( toa, 987.136 ) 19 | 20 | def test_13(self): 21 | toa = get_toa(32, 12, n_bw=250)["t_packet"] 22 | self.assertEqual( toa, 905.216 ) 23 | 24 | def test_14(self): 25 | toa = get_toa(32, 12, n_bw=125, enable_auto_ldro=False, enable_ldro=True)["t_packet"] 26 | self.assertEqual( toa, 1810.432 ) 27 | 28 | def test_21(self): 29 | toa = get_toa(64, 12, enable_eh=False)["t_packet"] 30 | self.assertEqual( toa, 2793.472 ) 31 | 32 | def test_22(self): 33 | toa = get_toa(64, 12, enable_crc=False)["t_packet"] 34 | self.assertEqual( toa, 2793.472 ) 35 | 36 | def test_31(self): 37 | toa = get_toa(12, 7)["t_packet"] 38 | self.assertEqual( toa, 41.216 ) 39 | 40 | def test_41(self): 41 | toa = get_toa(12, 7, n_bw=500, enable_eh=False, enable_crc=False, 42 | n_cr=4, n_preamble=6)["t_packet"] 43 | self.assertEqual( toa, 10.816 ) 44 | 45 | def test_99(self): 46 | toa = get_toa(8, 12, n_bw=500, 47 | enable_auto_ldro=False, enable_ldro=False, 48 | enable_eh=False, enable_crc=True, 49 | n_cr=1, n_preamble=6)["t_packet"] 50 | self.assertEqual( toa, 190.464 ) 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /toa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from lorawan_toa_cal import get_toa, parse_args 4 | 5 | if __name__ == "__main__" : 6 | opt = parse_args() 7 | if opt.sf != "FSK": 8 | ret = get_toa(opt.n_size, opt.n_sf, n_bw=opt.n_bw, 9 | enable_auto_ldro=opt.enable_auto_ldro, 10 | enable_ldro=opt.enable_ldro, 11 | enable_eh=opt.enable_eh, enable_crc=opt.enable_crc, 12 | n_cr=opt.n_cr, n_preamble=opt.n_preamble) 13 | ret["duty_cycle"] = opt.n_duty_cycle 14 | ret["t_cycle"] = (ret["t_packet"]/1000.)*(100./ret["duty_cycle"]) 15 | ret["max_packets_day"] = 86400./ret["t_cycle"] 16 | if opt.f_verbose: 17 | print("PHY payload size : %d Bytes" % ret["phy_pl_size"]) 18 | print("MAC payload size : %d Bytes" % ret["mac_pl_size"]) 19 | print("Spreading Factor : %d" % ret["sf"]) 20 | print("Band width : %d kHz" % ret["bw"]) 21 | print("Low data rate opt. : %s" % ret["ldro"]) 22 | print("Explicit header : %s" % ret["eh"]) 23 | print("CR (coding rate) : %d (4/%d)" % (ret["cr"], 4+ret["cr"])) 24 | print("Symbol Rate : %.3f symbol/s" % ret["r_sym"]) 25 | print("Symbol Time : %.3f msec/symbol" % ret["t_sym"]) 26 | print("Preamble size : %d symbols" % ret["n_preamble"]) 27 | print("Packet symbol size : %d symbols" % ret["n_sym_payload"]) 28 | print("Preamble ToA : %.3f msec" % ret["t_preamble"]) 29 | print("Payload ToA : %.3f msec" % ret["t_payload"]) 30 | print("Time on Air : %.3f msec" % ret["t_packet"]) 31 | print("RAW data rate : %.3f bps" % ret["raw_datarate"]) 32 | print("Duty Cycle : %d %%" % ret["duty_cycle"]) 33 | print("Min span of a cycle : %.3f sec" % ret["t_cycle"]) 34 | print("Max Frames per day : %d frames" % ret["max_packets_day"]) 35 | if opt.debug_level: 36 | ret0 = get_toa(0, opt.n_sf, n_bw=opt.n_bw, 37 | enable_auto_ldro=opt.enable_auto_ldro, 38 | enable_ldro=opt.enable_ldro, 39 | enable_eh=opt.enable_eh, enable_crc=opt.enable_crc, 40 | n_cr=opt.n_cr, n_preamble=opt.n_preamble) 41 | print("PHY PL=0 ToA : %.3f msec" % (ret0["t_packet"])) 42 | # preamble=8 cr=1? payload-len=7? crc=16 (payload) payload-crc=16 43 | # =? 48 ==> 6 bytes ? 44 | t0 = (ret["t_packet"]-ret0["t_packet"])/1000. 45 | print("PHY fr.dr.(48b) 7:15: %.3f bps" % ((8.*opt.n_size+48)/t0)) 46 | print("MAC frame DR : %.3f bps" % ((8.*(opt.n_size))/t0)) 47 | print("before ceil(x) : %.3f" % ret["v_ceil"]) 48 | else: 49 | print("%.3f" % ret["t_packet"]) 50 | else: # FSK 51 | raise NotImplementedError 52 | pass 53 | --------------------------------------------------------------------------------