├── examples ├── hello_world │ └── main.py └── echo │ └── main.py ├── LICENSE ├── README.md └── lora.py /examples/hello_world/main.py: -------------------------------------------------------------------------------- 1 | # Send "Hello world!" using the default LoRa parameters every second. 2 | # 3 | # The pin configuration used here is for the first LoRa module of these boards: 4 | # https://makerfabs.com/esp32-lora-gateway.html 5 | 6 | from lora import LoRa 7 | from machine import Pin, SPI 8 | from time import sleep 9 | 10 | # SPI pins 11 | SCK = 14 12 | MOSI = 13 13 | MISO = 12 14 | # Chip select 15 | CS = 32 16 | # Receive IRQ 17 | RX = 36 18 | 19 | # Setup SPI 20 | spi = SPI( 21 | 1, 22 | baudrate=10000000, 23 | sck=Pin(SCK, Pin.OUT, Pin.PULL_DOWN), 24 | mosi=Pin(MOSI, Pin.OUT, Pin.PULL_UP), 25 | miso=Pin(MISO, Pin.IN, Pin.PULL_UP), 26 | ) 27 | spi.init() 28 | 29 | # Setup LoRa 30 | lora = LoRa( 31 | spi, 32 | cs=Pin(CS, Pin.OUT), 33 | rx=Pin(RX, Pin.IN), 34 | ) 35 | 36 | while True: 37 | lora.send('Hello world!') 38 | sleep(1) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 davy wybiral 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 | -------------------------------------------------------------------------------- /examples/echo/main.py: -------------------------------------------------------------------------------- 1 | # Echo any messages received (using custom LoRa parameters). 2 | # 3 | # The pin configuration used here is for the first LoRa module of these boards: 4 | # https://makerfabs.com/esp32-lora-gateway.html 5 | 6 | from lora import LoRa 7 | from machine import Pin, SPI 8 | from time import sleep 9 | 10 | # SPI pins 11 | SCK = 14 12 | MOSI = 13 13 | MISO = 12 14 | # Chip select 15 | CS = 32 16 | # Receive IRQ 17 | RX = 36 18 | 19 | # Setup SPI 20 | spi = SPI( 21 | 1, 22 | baudrate=10000000, 23 | sck=Pin(SCK, Pin.OUT, Pin.PULL_DOWN), 24 | mosi=Pin(MOSI, Pin.OUT, Pin.PULL_UP), 25 | miso=Pin(MISO, Pin.IN, Pin.PULL_UP), 26 | ) 27 | spi.init() 28 | 29 | # Setup LoRa 30 | lora = LoRa( 31 | spi, 32 | cs=Pin(CS, Pin.OUT), 33 | rx=Pin(RX, Pin.IN), 34 | frequency=915.0, 35 | bandwidth=250000, 36 | spreading_factor=10, 37 | coding_rate=5, 38 | ) 39 | 40 | # Receive handler 41 | def handler(x): 42 | # Echo message 43 | lora.send(x) 44 | # Put module back in recv mode 45 | lora.recv() 46 | 47 | # Set handler 48 | lora.on_recv(handler) 49 | # Put module in recv mode 50 | lora.recv() 51 | 52 | # No need for main loop, code is asynchronous -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micropython-lora 2 | MicroPython library for controlling a Semtech SX127x LoRa module over SPI. 3 | 4 | The logic for the code came from [this module](https://github.com/Wei1234c/SX127x_driver_for_MicroPython_on_ESP8266) but was streamlined and rewritten to be more MicroPython-friendly. 5 | 6 | ## Examples 7 | 8 | ### Initialize 9 | 10 | The module requires an SPI bus connected to the SX127x, one pin to be `cs` (chip select), and one to be the `rx` (receive IRQ). 11 | 12 | ``` 13 | lora = LoRa( 14 | spi, 15 | cs=Pin(CS, Pin.OUT), 16 | rx=Pin(RX, Pin.IN), 17 | ) 18 | ``` 19 | 20 | ### Sending 21 | 22 | You can send bytes or a string (which will be encoded to bytes). A ValueError exception will be raised if the message exceeds the allowed payload length. Currently this method blocks until the message is sent. 23 | 24 | ``` 25 | lora.send('Hello world!') 26 | ``` 27 | 28 | ### Receiving 29 | 30 | Receiving is done by attaching a handler using `on_recv` and then calling `recv` to put the device in receive mode. Receive mode is non-blocking so other code can run after calling `recv` but if you call `send` afterward you will need to put the device back into receive mode again. 31 | 32 | ``` 33 | def handler(x): 34 | print(x) 35 | 36 | lora.on_recv(handler) 37 | lora.recv() 38 | ``` 39 | -------------------------------------------------------------------------------- /lora.py: -------------------------------------------------------------------------------- 1 | import gc 2 | from machine import Pin 3 | from time import sleep, sleep_ms 4 | 5 | TX_BASE_ADDR = 0x00 6 | RX_BASE_ADDR = 0x00 7 | 8 | PA_BOOST = 0x80 9 | PA_OUTPUT_RFO_PIN = 0 10 | PA_OUTPUT_PA_BOOST_PIN = 1 11 | 12 | REG_FIFO = 0x00 13 | REG_OP_MODE = 0x01 14 | REG_FRF_MSB = 0x06 15 | REG_FRF_MID = 0x07 16 | REG_FRF_LSB = 0x08 17 | REG_PA_CONFIG = 0x09 18 | REG_LNA = 0x0c 19 | REG_FIFO_ADDR_PTR = 0x0d 20 | REG_FIFO_TX_BASE_ADDR = 0x0e 21 | REG_FIFO_RX_BASE_ADDR = 0x0f 22 | REG_FIFO_RX_CURRENT_ADDR = 0x10 23 | REG_IRQ_FLAGS = 0x12 24 | REG_RX_NB_BYTES = 0x13 25 | REG_PKT_RSSI_VALUE = 0x1a 26 | REG_PKT_SNR_VALUE = 0x1b 27 | REG_MODEM_CONFIG_1 = 0x1d 28 | REG_MODEM_CONFIG_2 = 0x1e 29 | REG_PREAMBLE_MSB = 0x20 30 | REG_PREAMBLE_LSB = 0x21 31 | REG_PAYLOAD_LENGTH = 0x22 32 | REG_MODEM_CONFIG_3 = 0x26 33 | REG_DETECTION_OPTIMIZE = 0x31 34 | REG_DETECTION_THRESHOLD = 0x37 35 | REG_SYNC_WORD = 0x39 36 | REG_DIO_MAPPING_1 = 0x40 37 | REG_VERSION = 0x42 38 | 39 | MODE_LORA = 0x80 40 | MODE_SLEEP = 0x00 41 | MODE_STDBY = 0x01 42 | MODE_TX = 0x03 43 | MODE_RX_CONTINUOUS = 0x05 44 | 45 | IRQ_TX_DONE_MASK = 0x08 46 | IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20 47 | 48 | MAX_PKT_LENGTH = 255 49 | 50 | class LoRa: 51 | 52 | def __init__(self, spi, **kw): 53 | self.spi = spi 54 | self.cs = kw['cs'] 55 | self.rx = kw['rx'] 56 | while self._read(REG_VERSION) != 0x12: 57 | time.sleep_ms(100) 58 | #raise Exception('Invalid version or bad SPI connection') 59 | self.sleep() 60 | self.set_frequency(kw.get('frequency', 915.0)) 61 | self.set_bandwidth(kw.get('bandwidth', 250000)) 62 | self.set_spreading_factor(kw.get('spreading_factor', 10)) 63 | self.set_coding_rate(kw.get('coding_rate', 5)) 64 | self.set_preamble_length(kw.get('preamble_length', 4)) 65 | self.set_crc(kw.get('crc', False)) 66 | # set LNA boost 67 | self._write(REG_LNA, self._read(REG_LNA) | 0x03) 68 | # set auto AGC 69 | self._write(REG_MODEM_CONFIG_3, 0x00) 70 | self.set_tx_power(kw.get('tx_power', 24)) 71 | self._implicit = kw.get('implicit', False) 72 | self.set_implicit(self._implicit) 73 | self.set_sync_word(kw.get('sync_word', 0x12)) 74 | self._on_recv = kw.get('on_recv', None) 75 | self._write(REG_FIFO_TX_BASE_ADDR, TX_BASE_ADDR) 76 | self._write(REG_FIFO_RX_BASE_ADDR, RX_BASE_ADDR) 77 | self.standby() 78 | 79 | def begin_packet(self): 80 | self.standby() 81 | self._write(REG_FIFO_ADDR_PTR, TX_BASE_ADDR) 82 | self._write(REG_PAYLOAD_LENGTH, 0) 83 | 84 | def end_packet(self): 85 | self._write(REG_OP_MODE, MODE_LORA | MODE_TX) 86 | while (self._read(REG_IRQ_FLAGS) & IRQ_TX_DONE_MASK) == 0: 87 | pass 88 | self._write(REG_IRQ_FLAGS, IRQ_TX_DONE_MASK) 89 | gc.collect() 90 | 91 | def write_packet(self, b): 92 | n = self._read(REG_PAYLOAD_LENGTH) 93 | m = len(b) 94 | p = MAX_PKT_LENGTH - TX_BASE_ADDR 95 | if n + m > p: 96 | raise ValueError('Max payload length is ' + str(p)) 97 | for i in range(m): 98 | self._write(REG_FIFO, b[i]) 99 | self._write(REG_PAYLOAD_LENGTH, n + m) 100 | 101 | def send(self, x): 102 | if isinstance(x, str): 103 | x = x.encode() 104 | self.begin_packet() 105 | self.write_packet(x) 106 | self.end_packet() 107 | 108 | def _get_irq_flags(self): 109 | f = self._read(REG_IRQ_FLAGS) 110 | self._write(REG_IRQ_FLAGS, f) 111 | return f 112 | 113 | def get_rssi(self): 114 | rssi = self._read(REG_PKT_RSSI_VALUE) 115 | if self._frequency >= 779.0: 116 | return rssi - 157 117 | return rssi - 164 118 | 119 | def get_snr(self): 120 | return self._read(REG_PKT_SNR_VALUE) * 0.25 121 | 122 | def standby(self): 123 | self._write(REG_OP_MODE, MODE_LORA | MODE_STDBY) 124 | 125 | def sleep(self): 126 | self._write(REG_OP_MODE, MODE_LORA | MODE_SLEEP) 127 | 128 | def set_tx_power(self, level, outputPin=PA_OUTPUT_PA_BOOST_PIN): 129 | if outputPin == PA_OUTPUT_RFO_PIN: 130 | level = min(max(level, 0), 14) 131 | self._write(REG_PA_CONFIG, 0x70 | level) 132 | else: 133 | level = min(max(level, 2), 17) 134 | self._write(REG_PA_CONFIG, PA_BOOST | (level - 2)) 135 | 136 | def set_frequency(self, frequency): 137 | self._frequency = frequency 138 | hz = frequency * 1000000.0 139 | x = round(hz / 61.03515625) 140 | self._write(REG_FRF_MSB, (x >> 16) & 0xff) 141 | self._write(REG_FRF_MID, (x >> 8) & 0xff) 142 | self._write(REG_FRF_LSB, x & 0xff) 143 | 144 | def set_spreading_factor(self, sf): 145 | if sf < 6 or sf > 12: 146 | raise ValueError('Spreading factor must be between 6-12') 147 | self._write(REG_DETECTION_OPTIMIZE, 0xc5 if sf == 6 else 0xc3) 148 | self._write(REG_DETECTION_THRESHOLD, 0x0c if sf == 6 else 0x0a) 149 | reg2 = self._read(REG_MODEM_CONFIG_2) 150 | self._write(REG_MODEM_CONFIG_2, (reg2 & 0x0f) | ((sf << 4) & 0xf0)) 151 | self._write(REG_MODEM_CONFIG_3, 0x08 if (sf>10 and self._bandwidth<250000) else 0x00) 152 | 153 | def set_bandwidth(self, bw): 154 | self._bandwidth = bw 155 | bws = (7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000) 156 | i = 9 157 | for j in range(len(bws)): 158 | if bw <= bws[j]: 159 | i = j 160 | break 161 | x = self._read(REG_MODEM_CONFIG_1) & 0x0f 162 | self._write(REG_MODEM_CONFIG_1, x | (i << 4)) 163 | 164 | def set_coding_rate(self, denom): 165 | denom = min(max(denom, 5), 8) 166 | cr = denom - 4 167 | reg1 = self._read(REG_MODEM_CONFIG_1) 168 | self._write(REG_MODEM_CONFIG_1, (reg1 & 0xf1) | (cr << 1)) 169 | 170 | def set_preamble_length(self, n): 171 | self._write(REG_PREAMBLE_MSB, (n >> 8) & 0xff) 172 | self._write(REG_PREAMBLE_LSB, (n >> 0) & 0xff) 173 | 174 | def set_crc(self, crc=False): 175 | modem_config_2 = self._read(REG_MODEM_CONFIG_2) 176 | if crc: 177 | config = modem_config_2 | 0x04 178 | else: 179 | config = modem_config_2 & 0xfb 180 | self._write(REG_MODEM_CONFIG_2, config) 181 | 182 | def set_sync_word(self, sw): 183 | self._write(REG_SYNC_WORD, sw) 184 | 185 | def set_implicit(self, implicit=False): 186 | if self._implicit != implicit: 187 | self._implicit = implicit 188 | modem_config_1 = self._read(REG_MODEM_CONFIG_1) 189 | if implicit: 190 | config = modem_config_1 | 0x01 191 | else: 192 | config = modem_config_1 & 0xfe 193 | self._write(REG_MODEM_CONFIG_1, config) 194 | 195 | def on_recv(self, callback): 196 | self._on_recv = callback 197 | if self.rx: 198 | if callback: 199 | self._write(REG_DIO_MAPPING_1, 0x00) 200 | self.rx.irq(handler=self._irq_recv, trigger=Pin.IRQ_RISING) 201 | else: 202 | self.rx.irq(handler=None, trigger=0) 203 | 204 | def recv(self): 205 | self._write(REG_OP_MODE, MODE_LORA | MODE_RX_CONTINUOUS) 206 | 207 | def _irq_recv(self, event_source): 208 | f = self._get_irq_flags() 209 | if f & IRQ_PAYLOAD_CRC_ERROR_MASK == 0: 210 | if self._on_recv: 211 | self._on_recv(self._read_payload()) 212 | 213 | def _read_payload(self): 214 | self._write(REG_FIFO_ADDR_PTR, self._read(REG_FIFO_RX_CURRENT_ADDR)) 215 | if self._implicit: 216 | n = self._read(REG_PAYLOAD_LENGTH) 217 | else: 218 | n = self._read(REG_RX_NB_BYTES) 219 | payload = bytearray() 220 | for i in range(n): 221 | payload.append(self._read(REG_FIFO)) 222 | gc.collect() 223 | return bytes(payload) 224 | 225 | def _transfer(self, addr, x=0x00): 226 | resp = bytearray(1) 227 | self.cs.value(0) 228 | self.spi.write(bytes([addr])) 229 | self.spi.write_readinto(bytes([x]), resp) 230 | self.cs.value(1) 231 | return resp 232 | 233 | def _read(self, addr): 234 | x = self._transfer(addr & 0x7f) 235 | return int.from_bytes(x, 'big') 236 | 237 | def _write(self, addr, x): 238 | self._transfer(addr | 0x80, x) 239 | --------------------------------------------------------------------------------