├── LightLora ├── __init__.py ├── spicontrol.py ├── lorautil.py └── sx127x.py ├── package.json ├── LICENSE ├── Examples └── lorarun.py └── README.md /LightLora/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["LightLora/__init__.py", "github:MZachmann/LightLora_MicroPython/LightLora/__init__.py"], 4 | ["LightLora/lorautil.py", "github:MZachmann/LightLora_MicroPython/LightLora/lorautil.py"], 5 | ["LightLora/spicontrol.py", "github:MZachmann/LightLora_MicroPython/LightLora/spicontrol.py"], 6 | ["LightLora/sx127x.py", "github:MZachmann/LightLora_MicroPython/LightLora/sx127x.py"] 7 | ], 8 | "version": "1.0.0", 9 | "deps": [] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mark Zachmann 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. -------------------------------------------------------------------------------- /Examples/lorarun.py: -------------------------------------------------------------------------------- 1 | import time 2 | from LightLora import lorautil 3 | 4 | isSendSynchro = False # is send synchronous? 5 | 6 | lr = lorautil.LoraUtil() # the LoraUtil object 7 | 8 | # a really ugly example using the LightLora micropython library 9 | # do: 10 | # import lorarun 11 | # lorarun.doreader() 12 | # to start running a loop test. Ctrl-C to stop. 13 | # this ping-pongs fully with the Arduino LightLora example 14 | 15 | def syncSend(lutil, txt) : 16 | ''' send a packet synchronously ''' 17 | lutil.sendPacket(0xff, 0x41, txt.encode()) 18 | if(isSendSynchro) : 19 | return # we're done here if sendpacket is synchronouse 20 | sendTime = 0 21 | while not lutil.isPacketSent() : 22 | time.sleep(.1) 23 | # after 2 seconds of waiting for send just give up 24 | sendTime = sendTime + 1 25 | if sendTime > 19 : 26 | break 27 | 28 | # this ping-pongs fully with the Arduino LightLora example 29 | def doreader(): 30 | global lr 31 | endt = time.time() + 2 32 | startTime = time.time() 33 | ctr = 0 34 | while True: 35 | if lr.isPacketAvailable(): 36 | packet = None 37 | try: 38 | packet = lr.readPacket() 39 | if packet and packet.msgTxt: 40 | txt = packet.msgTxt + str(ctr) 41 | syncSend(lr, txt) 42 | endt = time.time() + 4 43 | etime = str(int(time.time() - startTime)) 44 | print("@" + etime + "r=" + str(txt)) 45 | ctr = ctr + 1 46 | except Exception as ex: 47 | print(str(ex)) 48 | if time.time() > endt: 49 | txt = 'P Lora' + str(ctr) 50 | syncSend(lr, txt) 51 | ctr = ctr + 1 52 | endt = time.time() + 4 53 | else: 54 | time.sleep(.05) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A MicroPython Library for Controlling a Semtech SX127x LoRa Chip 2 | --- 3 | This is yet another library for controlling an SX127x Semtech chip. This is entirely interrupt driven (transmit and receive) with callbacks. 4 | 5 | For power usage, you can sleep the CPU while waiting for an interrupt during transmit and receive cycles. 6 | 7 | There is a nearly exact copy of this library for Arduino here https://github.com/MZachmann/LightLora_Arduino 8 | 9 | Installation 10 | -- 11 | The library (LightLora) folder can just be copied and used as-is in a project. 12 | 13 | Usage 14 | -- 15 | During setup call 16 | ```python 17 | from LightLora import lorautil 18 | 19 | lru = lorautil.LoraUtil() 20 | ``` 21 | During the loop you can 22 | ```python 23 | if lru.isPacketAvailable(): 24 | pkt = lru.readPacket() 25 | print(pkt.msgTxt) 26 | ... 27 | txt = "Hello World" 28 | lru.sendPacket(0xff, 0x11, txt.encode()) # random dst, src at 29 | ``` 30 | 31 | The packet definition is: 32 | ```python 33 | class LoraPacket: 34 | def __init__(self): 35 | self.srcAddress = None 36 | self.dstAddress = None 37 | self.srcLineCount = None 38 | self.payLength = None 39 | self.msgTxt = None 40 | self.rssi = None 41 | self.snr = None 42 | ``` 43 | 44 | Customization 45 | --- 46 | The ports for the LoRa device are set in spicontrol.py for now. 47 | 48 | The `_doTransmit` and `_doReceive` methods in lorautil.LoraUtil are the callbacks on interrupt. 49 | 50 | Changelog: 51 | Jul 3, 2018 - 52 | -- 53 | * In lorarun.py (the example) make lr a global so comm options can be changed manually before calling doreader() 54 | * Add a synchronous send method in lorarun.py since the sendPacket method is now asynchronous 55 | * add reset, sleep, isPacketSent, and setFrequency methods to LoraUtil to mimic the Arduino implementation 56 | * add power_pin option to sx127x parameter set 57 | * use signal bandwidth as integer 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /LightLora/spicontrol.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from machine import Pin, SPI 3 | 4 | ''' Pin assignments for SPI and LoRa board 5 | This refers to a Feather ESP32 Wroom board using 6 | 12,27,33 for IRQ,CS,RST respectively ''' 7 | PIN_ID_LORA_RESET = 33 8 | PIN_ID_LORA_SS = 27 9 | PIN_ID_SCK = 5 10 | PIN_ID_MOSI = 18 11 | PIN_ID_MISO = 19 12 | PIN_ID_LORA_DIO0 = 12 13 | ''' this is for a Heltec LoRa module ''' 14 | #PIN_ID_LORA_RESET = 14 15 | #PIN_ID_LORA_SS = 18 16 | #PIN_ID_SCK = 5 17 | #PIN_ID_MOSI = 27 18 | #PIN_ID_MISO = 19 19 | #PIN_ID_LORA_DIO0 = 26 20 | 21 | # loraconfig is the project definition for pins <-> hardware 22 | 23 | class SpiControl: 24 | ''' simple higher level spi stuff ''' 25 | def __init__(self): 26 | self.spi = SPI(1, baudrate=5000000, polarity=0, phase=0, bits=8, 27 | firstbit=SPI.MSB, 28 | sck=Pin(PIN_ID_SCK, Pin.OUT), 29 | mosi=Pin(PIN_ID_MOSI, Pin.OUT), 30 | miso=Pin(PIN_ID_MISO, Pin.IN)) 31 | self.pinss = Pin(PIN_ID_LORA_SS, Pin.OUT) 32 | self.pinrst = Pin(PIN_ID_LORA_RESET, Pin.OUT) 33 | 34 | # sx127x transfer is always write two bytes while reading the second byte 35 | # a read doesn't write the second byte. a write returns the prior value. 36 | # write register # = 0x80 | read register # 37 | def transfer(self, address, value=0x00): 38 | response = bytearray(1) 39 | self.pinss.value(0) # hold chip select low 40 | self.spi.write(bytearray([address])) # write register address 41 | self.spi.write_readinto(bytearray([value]), response) # write or read register walue 42 | self.pinss.value(1) 43 | return response 44 | 45 | # this doesn't belong here but it doesn't really belong anywhere, so put 46 | # it with the other loraconfig-ed stuff 47 | def getIrqPin(self): 48 | irqPin = Pin(PIN_ID_LORA_DIO0, Pin.IN) 49 | return irqPin 50 | 51 | # this doesn't belong here but it doesn't really belong anywhere, so put 52 | # it with the other loraconfig-ed stuff 53 | def initLoraPins(self): 54 | ''' initialize the pins for the LoRa device. ''' 55 | self.pinss.value(1) # initialize CS to high (off) 56 | self.pinrst.value(1) # do a reset pulse 57 | sleep(.01) 58 | self.pinrst.value(0) 59 | sleep(.01) 60 | self.pinrst.value(1) 61 | -------------------------------------------------------------------------------- /LightLora/lorautil.py: -------------------------------------------------------------------------------- 1 | ''' this adds a little high-level init to an sx1276 and it also 2 | packetizes messages with address headers''' 3 | from time import sleep 4 | from LightLora import spicontrol, sx127x 5 | 6 | class LoraPacket: 7 | def __init__(self): 8 | self.srcAddress = None 9 | self.dstAddress = None 10 | self.srcLineCount = None 11 | self.payLength = None 12 | self.msgTxt = None 13 | self.rssi = None 14 | self.snr = None 15 | 16 | def clear(self): 17 | self.msgTxt = '' 18 | 19 | class LoraUtil: 20 | ''' a LoraUtil object has an sx1276 and it can send and receive LoRa packets 21 | sendPacket -> send a string 22 | isPacketAvailable -> do we have a packet available? 23 | readPacket -> get the latest packet 24 | ''' 25 | def __init__(self): 26 | # just be neat and init variables in the __init__ 27 | self.linecounter = 0 28 | self.packet = None 29 | self.doneTransmit = False 30 | 31 | # init spi 32 | self.spic = spicontrol.SpiControl() 33 | # init lora 34 | params = {'tx_power_level': 5, 35 | 'frequency' : 915e6, 36 | 'signal_bandwidth': 125000, 37 | 'spreading_factor': 9, 38 | 'coding_rate': 8, 39 | 'power_pin' : 1, # boost pin is 1, non-boost pin is 0 40 | 'enable_CRC': True} 41 | self.lora = sx127x.SX127x(spiControl=self.spic, parameters=params) 42 | self.spic.initLoraPins() # init pins 43 | self.lora.init() 44 | self.lora.onReceive(self._doReceive) 45 | self.lora.onTransmit(self._doTransmit) 46 | # put into receive mode and wait for an interrupt 47 | self.lora.receive() 48 | 49 | # we received a packet, deal with it 50 | def _doReceive(self, sx12, pay): 51 | pkt = LoraPacket() 52 | self.packet = None 53 | if pay and len(pay) > 4: 54 | pkt.srcAddress = pay[0] 55 | pkt.dstAddress = pay[1] 56 | pkt.srcLineCount = pay[2] 57 | pkt.payLength = pay[3] 58 | pkt.rssi = sx12.packetRssi() 59 | pkt.snr = sx12.packetSnr() 60 | try: 61 | pkt.msgTxt = pay[4:].decode('utf-8', 'ignore') 62 | except Exception as ex: 63 | print("doReceiver error: ") 64 | print(ex) 65 | self.packet = pkt 66 | 67 | # the transmit ended 68 | def _doTransmit(self): 69 | self.doneTransmit = True 70 | self.lora.receive() # wait for a packet (?) 71 | 72 | def writeInt(self, value): 73 | self.lora.write(bytearray([value])) 74 | 75 | def sendPacket(self, dstAddress, localAddress, outGoing): 76 | '''send a packet of header info and a bytearray to dstAddress 77 | asynchronous. Returns immediately. ''' 78 | try: 79 | self.linecounter = self.linecounter + 1 80 | self.doneTransmit = False 81 | self.lora.beginPacket() 82 | self.writeInt(dstAddress) 83 | self.writeInt(localAddress) 84 | self.writeInt(self.linecounter) 85 | self.writeInt(len(outGoing)) 86 | self.lora.write(outGoing) 87 | self.lora.endPacket() 88 | except Exception as ex: 89 | print(str(ex)) 90 | 91 | def setFrequency(self, frequency) : 92 | ''' set the center frequency of the device. 902-928 for 915 band ''' 93 | self.lora.setFrequency(frequency) 94 | 95 | def sleep(self) : 96 | ''' sleep the device ''' 97 | self.lora.sleep() 98 | 99 | def reset(self) : 100 | ''' reset the device ''' 101 | self.spic.initLoraPins() # init pins 102 | 103 | def isPacketSent(self) : 104 | return self.doneTransmit 105 | 106 | def isPacketAvailable(self): 107 | ''' convert to bool result from none, true ''' 108 | return True if self.packet else False 109 | 110 | def readPacket(self): 111 | '''return the current packet (or none) and clear it out''' 112 | pkt = self.packet 113 | self.packet = None 114 | return pkt 115 | -------------------------------------------------------------------------------- /LightLora/sx127x.py: -------------------------------------------------------------------------------- 1 | '''This is a generic sx127x driver for the Semtech chipsets. 2 | In particular, it has a minor tweak for the sx1276. 3 | 4 | This code supports interrupt driven send and receive for maximum efficiency. 5 | Call onReceive and onTransmit to define the interrupt handlers. 6 | Receive handler gets a packet of data 7 | Transmit handler is informed the transmit ended 8 | 9 | Communications is handled by an SpiControl object wrapping SPI 10 | 11 | 12 | ''' 13 | import gc 14 | import _thread 15 | from machine import Pin 16 | 17 | PA_OUTPUT_RFO_PIN = 0 18 | PA_OUTPUT_PA_BOOST_PIN = 1 19 | 20 | # registers 21 | REG_FIFO = 0x00 22 | REG_OP_MODE = 0x01 23 | REG_FRF_MSB = 0x06 24 | REG_FRF_MID = 0x07 25 | REG_FRF_LSB = 0x08 26 | REG_PA_CONFIG = 0x09 27 | REG_OCP = 0x0b # overcurrent protection 28 | REG_LNA = 0x0c 29 | REG_FIFO_ADDR_PTR = 0x0d 30 | 31 | REG_FIFO_TX_BASE_ADDR = 0x0e 32 | FifoTxBaseAddr = 0x00 33 | # FifoTxBaseAddr = 0x80 34 | 35 | REG_FIFO_RX_BASE_ADDR = 0x0f 36 | FifoRxBaseAddr = 0x00 37 | REG_FIFO_RX_CURRENT_ADDR = 0x10 38 | REG_IRQ_FLAGS_MASK = 0x11 39 | REG_IRQ_FLAGS = 0x12 40 | REG_RX_NB_BYTES = 0x13 41 | REG_PKT_RSSI_VALUE = 0x1a 42 | REG_PKT_SNR_VALUE = 0x1b 43 | REG_MODEM_CONFIG_1 = 0x1d 44 | REG_MODEM_CONFIG_2 = 0x1e 45 | REG_PREAMBLE_MSB = 0x20 46 | REG_PREAMBLE_LSB = 0x21 47 | REG_PAYLOAD_LENGTH = 0x22 48 | REG_FIFO_RX_BYTE_ADDR = 0x25 49 | REG_MODEM_CONFIG_3 = 0x26 50 | REG_RSSI_WIDEBAND = 0x2c 51 | REG_DETECTION_OPTIMIZE = 0x31 52 | REG_DETECTION_THRESHOLD = 0x37 53 | REG_SYNC_WORD = 0x39 54 | REG_DIO_MAPPING_1 = 0x40 55 | REG_VERSION = 0x42 56 | REG_PA_DAC = 0x4d 57 | 58 | 59 | # modes 60 | MODE_LONG_RANGE_MODE = 0x80 # bit 7: 1 => LoRa mode 61 | MODE_SLEEP = 0x00 62 | MODE_STDBY = 0x01 63 | MODE_TX = 0x03 64 | MODE_RX_CONTINUOUS = 0x05 65 | # MODE_RX_SINGLE = 0x06 66 | # 6 is not supported on the 1276 67 | MODE_RX_SINGLE = 0x05 68 | 69 | # PA config 70 | PA_BOOST = 0x80 71 | 72 | # Low Data Rate flag 73 | LDO_FLAG = 8 74 | 75 | # IRQ masks 76 | IRQ_TX_DONE_MASK = 0x08 77 | IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20 78 | IRQ_RX_DONE_MASK = 0x40 79 | IRQ_RX_TIME_OUT_MASK = 0x80 80 | 81 | # Buffer size 82 | MAX_PKT_LENGTH = 255 83 | 84 | # pass in non-default parameters for any/all options in the constructor parameters argument 85 | DEFAULT_PARAMETERS = {'frequency': 915E6, 'tx_power_level': 2, 'signal_bandwidth': 125000, 86 | 'spreading_factor': 7, 'coding_rate': 5, 'preamble_length': 8, 87 | 'power_pin' : PA_OUTPUT_PA_BOOST_PIN, 88 | 'implicitHeader': False, 'sync_word': 0x12, 'enable_CRC': False} 89 | 90 | REQUIRED_VERSION = 0x12 91 | 92 | class SX127x: 93 | ''' Standard SX127x library. Requires an spicontrol.SpiControl instance for spiControl ''' 94 | def __init__(self, 95 | name='SX127x', 96 | parameters={}, 97 | onReceive=None, 98 | onTransmit=None, 99 | spiControl=None): 100 | 101 | self.name = name 102 | self.parameters = parameters 103 | self.bandwidth = 125000 # default bandwidth 104 | self.spreading = 6 # default spreading factor 105 | self._onReceive = onReceive # the onreceive function 106 | self._onTransmit = onTransmit # the ontransmit function 107 | self.doAcquire = hasattr(_thread, 'allocate_lock') # micropython vs loboris 108 | if self.doAcquire : 109 | self._lock = _thread.allocate_lock() 110 | else : 111 | self._lock = True 112 | self._spiControl = spiControl # the spi wrapper - see spicontrol.py 113 | self.irqPin = spiControl.getIrqPin() # a way to need loracontrol only in spicontrol 114 | self.isLoboris = not callable(getattr(self.irqPin, "irq", None)) # micropython vs loboris 115 | 116 | # if we passed in a param use it, else use default 117 | def _useParam(self, who): 118 | return DEFAULT_PARAMETERS[who] if not who in self.parameters.keys() else self.parameters[who] 119 | 120 | def init(self): 121 | # check version 122 | version = self.readRegister(REG_VERSION) 123 | if version != REQUIRED_VERSION: 124 | print("Detected version:", version) 125 | raise Exception('Invalid version.') 126 | 127 | # put in LoRa and sleep mode 128 | self.sleep() 129 | 130 | # set auto AGC before setting bandwidth and spreading factor 131 | # because they'll set the low-data-rate flag bit 132 | self.writeRegister(REG_MODEM_CONFIG_3, 0x04) 133 | 134 | # config 135 | self.setFrequency(self._useParam('frequency')) 136 | self.setSignalBandwidth(self._useParam('signal_bandwidth')) 137 | 138 | # set LNA boost 139 | self.writeRegister(REG_LNA, self.readRegister(REG_LNA) | 0x03) 140 | 141 | powerpin = self._useParam('power_pin') 142 | self.setTxPower(self._useParam('tx_power_level'), powerpin) 143 | self._implicitHeaderMode = None 144 | self.implicitHeaderMode(self._useParam('implicitHeader')) 145 | self.setSpreadingFactor(self._useParam('spreading_factor')) 146 | self.setCodingRate(self._useParam('coding_rate')) 147 | self.setPreambleLength(self._useParam('preamble_length')) 148 | self.setSyncWord(self._useParam('sync_word')) 149 | self.enableCRC(self._useParam('enable_CRC')) 150 | 151 | # set base addresses 152 | self.writeRegister(REG_FIFO_TX_BASE_ADDR, FifoTxBaseAddr) 153 | self.writeRegister(REG_FIFO_RX_BASE_ADDR, FifoRxBaseAddr) 154 | 155 | self.standby() 156 | 157 | # start sending a packet (reset the fifo address, go into standby) 158 | def beginPacket(self, implicitHeaderMode=False): 159 | self.standby() 160 | self.implicitHeaderMode(implicitHeaderMode) 161 | # reset FIFO address and paload length 162 | self.writeRegister(REG_FIFO_ADDR_PTR, FifoTxBaseAddr) 163 | self.writeRegister(REG_PAYLOAD_LENGTH, 0) 164 | 165 | # finished putting packet into fifo, send it 166 | # non-blocking so don't immediately receive... 167 | def endPacket(self): 168 | ''' non-blocking end packet ''' 169 | if self._onTransmit: 170 | # enable tx to raise DIO0 171 | self._prepIrqHandler(self._handleOnTransmit) # attach handler 172 | self.writeRegister(REG_DIO_MAPPING_1, 0x40) # enable transmit dio0 173 | else: 174 | self._prepIrqHandler(None) # no handler 175 | # put in TX mode 176 | self.writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_TX) 177 | 178 | def isTxDone(self): 179 | ''' if Tx is done return true, and clear irq register - so it only returns true once ''' 180 | if self._onTransmit: 181 | print("Do not call isTxDone with transmit interrupts enabled. Use the callback.") 182 | return False 183 | irqFlags = self.getIrqFlags() 184 | if (irqFlags & IRQ_TX_DONE_MASK) == 0: 185 | return False 186 | # clear IRQ's 187 | self.collect_garbage() 188 | return True 189 | 190 | def write(self, buffer): 191 | currentLength = self.readRegister(REG_PAYLOAD_LENGTH) 192 | size = len(buffer) 193 | # check size 194 | size = min(size, (MAX_PKT_LENGTH - FifoTxBaseAddr - currentLength)) 195 | # write data 196 | for i in range(size): 197 | self.writeRegister(REG_FIFO, buffer[i]) 198 | # update length 199 | self.writeRegister(REG_PAYLOAD_LENGTH, currentLength + size) 200 | return size 201 | 202 | def acquire_lock(self, lock=False): 203 | if self._lock: 204 | # we have a lock object 205 | if self.doAcquire: 206 | if lock: 207 | self._lock.acquire() 208 | else: 209 | self._lock.release() 210 | # else lock the thread hard 211 | else: 212 | if lock: 213 | _thread.lock() 214 | else: 215 | _thread.unlock() 216 | 217 | def println(self, string, implicitHeader=False): 218 | self.acquire_lock(True) # wait until RX_Done, lock and begin writing. 219 | self.beginPacket(implicitHeader) 220 | self.write(string.encode()) 221 | self.endPacket() 222 | self.acquire_lock(False) # unlock when done writing 223 | 224 | def getIrqFlags(self): 225 | ''' get and reset the irq register ''' 226 | irqFlags = self.readRegister(REG_IRQ_FLAGS) 227 | self.writeRegister(REG_IRQ_FLAGS, irqFlags) 228 | return irqFlags 229 | 230 | def packetRssi(self): 231 | return self.readRegister(REG_PKT_RSSI_VALUE) - (164 if self._frequency < 868E6 else 157) 232 | 233 | def packetSnr(self): 234 | return self.readRegister(REG_PKT_SNR_VALUE) * 0.25 235 | 236 | def standby(self): 237 | self.writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_STDBY) 238 | 239 | def sleep(self): 240 | self.writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_SLEEP) 241 | 242 | def setTxPower(self, level, outputPin=PA_OUTPUT_PA_BOOST_PIN): 243 | if outputPin == PA_OUTPUT_RFO_PIN: 244 | # RFO 245 | level = min(max(level, 0), 14) 246 | self.writeRegister(REG_PA_CONFIG, 0x70 | level) 247 | else: 248 | # PA BOOST 2...20 are valid values 249 | level = min(max(level, 2), 20) 250 | dacValue = self.readRegister(REG_PA_DAC) & ~7 251 | ocpValue = 0 252 | if level > 17: 253 | dacValue = dacValue | 7 254 | ocpValue = 0x20 | 18 # 150 ma [-30 + 10*value] 255 | level = level - 5 # normalize to 15 max 256 | else: 257 | dacValue = dacValue | 4 258 | ocpValue = 11 # 100 mA [45 + 5*value] 259 | level = level - 2 # normalize to 15 max 260 | self.writeRegister(REG_PA_CONFIG, PA_BOOST | level) 261 | self.writeRegister(REG_PA_DAC, dacValue) 262 | self.writeRegister(REG_OCP, ocpValue) 263 | 264 | # set the frequency band. passed in Hz 265 | # Frf register setting = Freq / FSTEP where 266 | # FSTEP = FXOSC/2**19 where FXOSC=32MHz. So FSTEP==61.03515625 267 | def setFrequency(self, frequency): 268 | self._frequency = frequency 269 | frfs = (int)(frequency / 61.03515625) 270 | self.writeRegister(REG_FRF_MSB, frfs >> 16) 271 | self.writeRegister(REG_FRF_MID, frfs >> 8) 272 | self.writeRegister(REG_FRF_LSB, frfs) 273 | 274 | def setSpreadingFactor(self, sf): 275 | sf = min(max(sf, 6), 12) 276 | self.spreading = sf 277 | self.writeRegister(REG_DETECTION_OPTIMIZE, 0xc5 if sf == 6 else 0xc3) 278 | self.writeRegister(REG_DETECTION_THRESHOLD, 0x0c if sf == 6 else 0x0a) 279 | self.writeRegister(REG_MODEM_CONFIG_2, (self.readRegister(REG_MODEM_CONFIG_2) & 0x0f) | ((sf << 4) & 0xf0)) 280 | self.setLdoFlag() 281 | 282 | def setSignalBandwidth(self, sbw): 283 | bins = (7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000, 500000) 284 | bw = 9 # default to 500000 max 285 | for i in range(len(bins)): 286 | if sbw <= bins[i]: 287 | bw = i 288 | break 289 | self.bandwidth = bins[bw] 290 | self.writeRegister(REG_MODEM_CONFIG_1, (self.readRegister(REG_MODEM_CONFIG_1) & 0x0f) | (bw << 4)) 291 | self.setLdoFlag() 292 | 293 | def setLdoFlag(self): 294 | ''' set the low data rate flag. This must be 1 if symbol duration is > 16ms ''' 295 | symbolDuration = 1000 / (self.bandwidth / (1 << self.spreading)) 296 | config3 = self.readRegister(REG_MODEM_CONFIG_3) & ~LDO_FLAG 297 | if symbolDuration > 16: 298 | config3 = config3 | LDO_FLAG 299 | self.writeRegister(REG_MODEM_CONFIG_3, config3) 300 | 301 | def setCodingRate(self, denominator): 302 | ''' this takes a value of 5..8 as the denominator of 4/5, 4/6, 4/7, 5/8 ''' 303 | denominator = min(max(denominator, 5), 8) 304 | cr = denominator - 4 305 | self.writeRegister(REG_MODEM_CONFIG_1, (self.readRegister(REG_MODEM_CONFIG_1) & 0xf1) | (cr << 1)) 306 | 307 | def setPreambleLength(self, length): 308 | self.writeRegister(REG_PREAMBLE_MSB, (length >> 8) & 0xff) 309 | self.writeRegister(REG_PREAMBLE_LSB, (length >> 0) & 0xff) 310 | 311 | def enableCRC(self, enable_CRC=False): 312 | modem_config_2 = self.readRegister(REG_MODEM_CONFIG_2) 313 | config = modem_config_2 | 0x04 if enable_CRC else modem_config_2 & 0xfb 314 | self.writeRegister(REG_MODEM_CONFIG_2, config) 315 | 316 | def setSyncWord(self, sw): 317 | self.writeRegister(REG_SYNC_WORD, sw) 318 | 319 | def dumpRegisters(self): 320 | for i in range(128): 321 | print("0x{0:02x}: {1:02x}".format(i, self.readRegister(i))) 322 | 323 | def implicitHeaderMode(self, implicitHeaderMode=False): 324 | if self._implicitHeaderMode != implicitHeaderMode: # set value only if different. 325 | self._implicitHeaderMode = implicitHeaderMode 326 | modem_config_1 = self.readRegister(REG_MODEM_CONFIG_1) 327 | config = modem_config_1 | 0x01 if implicitHeaderMode else modem_config_1 & 0xfe 328 | self.writeRegister(REG_MODEM_CONFIG_1, config) 329 | 330 | def _prepIrqHandler(self, handlefn): 331 | ''' attach the handler to the irq pin, disable if None ''' 332 | if self.irqPin: 333 | if handlefn: 334 | if self.isLoboris: 335 | self.irqPin.init(handler=handlefn, trigger=Pin.IRQ_RISING) 336 | else: 337 | self.irqPin.irq(handler=handlefn, trigger=Pin.IRQ_RISING) 338 | else: 339 | if self.isLoboris: 340 | self.irqPin.init(handler=None, trigger=0) 341 | else: 342 | self.irqPin.irq(handler=handlefn, trigger=0) 343 | 344 | 345 | def onReceive(self, callback): 346 | ''' establish a callback function for receive interrupts''' 347 | self._onReceive = callback 348 | self._prepIrqHandler(None) # in case we have one and we're receiving. stop. 349 | 350 | def onTransmit(self, callback): 351 | ''' establish a callback function for transmit interrupts''' 352 | self._onTransmit = callback 353 | 354 | def receive(self, size=0): 355 | ''' enable reception - call this when you want to receive stuff ''' 356 | self.implicitHeaderMode(size > 0) 357 | if size > 0: 358 | self.writeRegister(REG_PAYLOAD_LENGTH, size & 0xff) 359 | # enable rx to raise DIO0 360 | if self._onReceive: 361 | self._prepIrqHandler(self._handleOnReceive) # attach handler 362 | self.writeRegister(REG_DIO_MAPPING_1, 0x00) 363 | else: 364 | self._prepIrqHandler(None) # no handler 365 | # The last packet always starts at FIFO_RX_CURRENT_ADDR 366 | # no need to reset FIFO_ADDR_PTR 367 | self.writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_CONTINUOUS) 368 | 369 | # got a receive interrupt, handle it 370 | def _handleOnReceive(self, event_source): 371 | self.acquire_lock(True) # lock until TX_Done 372 | irqFlags = self.getIrqFlags() 373 | irqBad = IRQ_PAYLOAD_CRC_ERROR_MASK | IRQ_RX_TIME_OUT_MASK 374 | if (irqFlags & IRQ_RX_DONE_MASK) and \ 375 | ((irqFlags & irqBad) == 0) and \ 376 | self._onReceive: 377 | # it's a receive data ready interrupt 378 | payload = self.read_payload() 379 | self.acquire_lock(False) # unlock when done reading 380 | self._onReceive(self, payload) 381 | else: 382 | self.acquire_lock(False) # unlock in any case. 383 | if not irqFlags & IRQ_RX_DONE_MASK: 384 | print("not rx done mask") 385 | elif (irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK) != 0: 386 | print("crc error") 387 | elif (irqFlags & IRQ_RX_TIME_OUT_MASK) != 0: 388 | print("receive timeout error") 389 | else: 390 | print("no receive method defined") 391 | 392 | # Got a transmit interrupt, handle it 393 | def _handleOnTransmit(self, event_source): 394 | self.acquire_lock(True) # lock until flags cleared 395 | irqFlags = self.getIrqFlags() 396 | if irqFlags & IRQ_TX_DONE_MASK: 397 | # it's a transmit finish interrupt 398 | self._prepIrqHandler(None) # disable handler since we're done 399 | self.acquire_lock(False) # unlock 400 | if self._onTransmit: 401 | self._onTransmit() 402 | else: 403 | print("transmit callback but no callback method") 404 | else: 405 | self.acquire_lock(False) # unlock 406 | print("transmit callback but not txdone: " + str(irqFlags)) 407 | 408 | def receivedPacket(self, size=0): 409 | ''' when no receive handler, this tells if packet ready. Preps for receive''' 410 | if self._onReceive: 411 | print("Do not call receivedPacket. Use the callback.") 412 | return False 413 | irqFlags = self.getIrqFlags() 414 | self.implicitHeaderMode(size > 0) 415 | if size > 0: 416 | self.writeRegister(REG_PAYLOAD_LENGTH, size & 0xff) 417 | # if (irqFlags & IRQ_RX_DONE_MASK) and \ 418 | # (irqFlags & IRQ_RX_TIME_OUT_MASK == 0) and \ 419 | # (irqFlags & IRQ_PAYLOAD_CRC_ERROR_MASK == 0): 420 | if irqFlags == IRQ_RX_DONE_MASK: # RX_DONE only, irqFlags should be 0x40 421 | # automatically standby when RX_DONE 422 | return True 423 | elif self.readRegister(REG_OP_MODE) != (MODE_LONG_RANGE_MODE | MODE_RX_SINGLE): 424 | # no packet received and not in receive mode 425 | # reset FIFO address / # enter single RX mode 426 | self.writeRegister(REG_FIFO_ADDR_PTR, FifoRxBaseAddr) 427 | self.writeRegister(REG_OP_MODE, MODE_LONG_RANGE_MODE | MODE_RX_SINGLE) 428 | return False 429 | 430 | def read_payload(self): 431 | # set FIFO address to current RX address 432 | # fifo_rx_current_addr = self.readRegister(REG_FIFO_RX_CURRENT_ADDR) 433 | self.writeRegister(REG_FIFO_ADDR_PTR, self.readRegister(REG_FIFO_RX_CURRENT_ADDR)) 434 | # read packet length 435 | packetLength = self.readRegister(REG_PAYLOAD_LENGTH) if self._implicitHeaderMode else \ 436 | self.readRegister(REG_RX_NB_BYTES) 437 | payload = bytearray() 438 | for i in range(packetLength): 439 | payload.append(self.readRegister(REG_FIFO)) 440 | self.collect_garbage() 441 | return bytes(payload) 442 | 443 | def readRegister(self, address, byteorder='big', signed=False): 444 | response = self._spiControl.transfer(address & 0x7f) 445 | return int.from_bytes(response, byteorder) 446 | 447 | def writeRegister(self, address, value): 448 | self._spiControl.transfer(address | 0x80, value) 449 | 450 | def collect_garbage(self): 451 | gc.collect() 452 | #print('[Memory - free: {} allocated: {}]'.format(gc.mem_free(), gc.mem_alloc())) 453 | --------------------------------------------------------------------------------