├── .gitignore ├── SX127x ├── __init__.py ├── LoRaArgumentParser.py ├── board_config.py ├── constants.py └── LoRa.py ├── LoRaWAN ├── __init__.py ├── MalformedPacketException.py ├── Direction.py ├── MHDR.py ├── JoinRequestPayload.py ├── FHDR.py ├── AES_CMAC.py ├── MacPayload.py ├── JoinAcceptPayload.py ├── DataPayload.py └── PhyPayload.py ├── reset.py ├── README.md ├── tx_ttn.py ├── rx_ttn.py └── otaa_ttn.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /SX127x/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['SX127x'] 2 | -------------------------------------------------------------------------------- /LoRaWAN/__init__.py: -------------------------------------------------------------------------------- 1 | from .PhyPayload import PhyPayload 2 | 3 | def new(nwkey = [], appkey = []): 4 | return PhyPayload(nwkey, appkey) 5 | -------------------------------------------------------------------------------- /LoRaWAN/MalformedPacketException.py: -------------------------------------------------------------------------------- 1 | class MalformedPacketException(Exception): 2 | 3 | def __init__(self, msg = ""): 4 | Exception.__init__(self, msg) 5 | -------------------------------------------------------------------------------- /reset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import RPi.GPIO as GPIO 3 | import time 4 | 5 | GPIO.setmode(GPIO.BCM) 6 | GPIO.setup(17, GPIO.OUT) 7 | GPIO.output(17, GPIO.HIGH) 8 | time.sleep(.100) 9 | GPIO.output(17, GPIO.LOW) 10 | GPIO.cleanup() 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoRaWAN 2 | This is a LoRaWAN v1.0 implementation in python. 3 | 4 | It uses https://github.com/mayeranalytics/pySX127x and it's currently being tested with a RFM95 attached to a Raspberry PI. 5 | 6 | See: https://www.lora-alliance.org/portals/0/specs/LoRaWAN%20Specification%201R0.pdf 7 | 8 | ## Installation 9 | Just git clone and check rx_ttn.py for reading LoRaWAN messages and tx_ttn.py for sending LoRaWAN messages. 10 | 11 | ## TODO 12 | Make code more readable and easier to use 13 | -------------------------------------------------------------------------------- /LoRaWAN/Direction.py: -------------------------------------------------------------------------------- 1 | from .MHDR import MHDR 2 | 3 | class Direction: 4 | 5 | UP = 0x00 6 | DOWN = 0x01 7 | DIRECTION = { 8 | MHDR.JOIN_REQUEST: UP, 9 | MHDR.JOIN_ACCEPT: DOWN, 10 | MHDR.UNCONF_DATA_UP: UP, 11 | MHDR.UNCONF_DATA_DOWN: DOWN, 12 | MHDR.CONF_DATA_UP: UP, 13 | MHDR.CONF_DATA_DOWN: DOWN, 14 | MHDR.RFU: UP, 15 | MHDR.PROPRIETARY: UP } 16 | 17 | def __init__(self, mhdr): 18 | self.set(mhdr) 19 | 20 | def get(self): 21 | return self.direction 22 | 23 | def set(self, mhdr): 24 | self.direction = self.DIRECTION[mhdr.get_mtype()] 25 | -------------------------------------------------------------------------------- /LoRaWAN/MHDR.py: -------------------------------------------------------------------------------- 1 | from .MalformedPacketException import MalformedPacketException 2 | 3 | class MHDR: 4 | 5 | LORAWAN_V1 = 0x00; 6 | 7 | MHDR_TYPE = 0xE0; 8 | MHDR_RFU = 0x1C; 9 | MHDR_MAJOR = 0x03; 10 | 11 | JOIN_REQUEST = 0x00 12 | JOIN_ACCEPT = 0x20 13 | UNCONF_DATA_UP = 0x40 14 | UNCONF_DATA_DOWN = 0x60 15 | CONF_DATA_UP = 0x80 16 | CONF_DATA_DOWN = 0xA0 17 | RFU = 0xC0 # rejoin for roaming 18 | PROPRIETARY = 0xE0 19 | 20 | def __init__(self, mhdr): 21 | self.mhdr = mhdr 22 | mversion = mhdr & self.MHDR_MAJOR 23 | if mversion != self.LORAWAN_V1: 24 | raise MalformedPacketException("Invalid major version") 25 | 26 | def to_raw(self): 27 | return self.mhdr 28 | 29 | def get_mversion(self): 30 | return self.mhdr & self.MHDR_MAJOR 31 | 32 | def get_mtype(self): 33 | return self.mhdr & self.MHDR_TYPE 34 | -------------------------------------------------------------------------------- /LoRaWAN/JoinRequestPayload.py: -------------------------------------------------------------------------------- 1 | # 2 | # frm_payload: appeui(8) deveui(8) devnonce(2) 3 | # 4 | from .MalformedPacketException import MalformedPacketException 5 | from .AES_CMAC import AES_CMAC 6 | from Crypto.Cipher import AES 7 | 8 | class JoinRequestPayload: 9 | 10 | def read(self, payload): 11 | if len(payload) != 18: 12 | raise MalformedPacketException("Invalid join request"); 13 | self.deveui = payload[8:16] 14 | self.appeui = payload[:8] 15 | self.devnonce = payload[16:18] 16 | 17 | def create(self, args): 18 | self.deveui = list(reversed(args['deveui'])) 19 | self.appeui = list(reversed(args['appeui'])) 20 | self.devnonce = args['devnonce'] 21 | 22 | def length(self): 23 | return 18 24 | 25 | def to_raw(self): 26 | payload = [] 27 | payload += self.appeui 28 | payload += self.deveui 29 | payload += self.devnonce 30 | return payload 31 | 32 | def get_appeui(self): 33 | return self.appeui 34 | 35 | def get_deveui(self): 36 | return self.deveui 37 | 38 | def get_devnonce(self): 39 | return self.devnonce 40 | 41 | def compute_mic(self, key, direction, mhdr): 42 | mic = [mhdr.to_raw()] 43 | mic += self.to_raw() 44 | 45 | cmac = AES_CMAC() 46 | computed_mic = cmac.encode(bytes(key), bytes(mic))[:4] 47 | return list(map(int, computed_mic)) 48 | 49 | def decrypt_payload(self, key, direction, mic): 50 | return self.to_raw() 51 | -------------------------------------------------------------------------------- /LoRaWAN/FHDR.py: -------------------------------------------------------------------------------- 1 | # 2 | # fhdr: devaddr(4) fctrl(1) fcnt(2) fopts(0..N) 3 | # 4 | from .MalformedPacketException import MalformedPacketException 5 | from struct import unpack 6 | from .MHDR import MHDR 7 | 8 | class FHDR: 9 | 10 | def read(self, mac_payload): 11 | if len(mac_payload) < 7: 12 | raise MalformedPacketException("Invalid fhdr") 13 | 14 | self.devaddr = mac_payload[:4] 15 | self.fctrl = mac_payload[4] 16 | self.fcnt = mac_payload[5:7] 17 | self.fopts = mac_payload[7:7 + (self.fctrl & 0xf)] 18 | 19 | def create(self, mtype, args): 20 | self.devaddr = [0x00, 0x00, 0x00, 0x00] 21 | self.fctrl = 0x00 22 | if 'fcnt' in args: 23 | self.fcnt = args['fcnt'].to_bytes(2, byteorder='little') 24 | else: 25 | self.fcnt = [0x00, 0x00] 26 | self.fopts = [] 27 | if mtype == MHDR.UNCONF_DATA_UP or mtype == MHDR.UNCONF_DATA_DOWN or\ 28 | mtype == MHDR.CONF_DATA_UP or mtype == MHDR.CONF_DATA_DOWN: 29 | self.devaddr = list(reversed(args['devaddr'])) 30 | 31 | def length(self): 32 | return 4 + 1 + 2 + (self.fctrl & 0xf) 33 | 34 | def to_raw(self): 35 | fhdr = [] 36 | fhdr += self.devaddr 37 | fhdr += [self.fctrl] 38 | fhdr += self.fcnt 39 | if self.fopts: 40 | fhdr += self.fopts 41 | return fhdr 42 | 43 | def get_devaddr(self): 44 | return self.devaddr 45 | 46 | def set_devaddr(self, devaddr): 47 | self.devaddr = devaddr 48 | 49 | def get_fctrl(self): 50 | return self.fctrl 51 | 52 | def set_fctrl(self, fctrl): 53 | self.fctrl = fctrl 54 | 55 | def get_fcnt(self): 56 | return self.fcnt 57 | 58 | def set_fcnt(self, fcnt): 59 | self.fcnt = fcnt 60 | 61 | def get_fopts(self): 62 | return self.fopts 63 | 64 | def set_fopts(self, fopts): 65 | self.fopts = fopts 66 | -------------------------------------------------------------------------------- /tx_ttn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | from time import sleep 4 | from SX127x.LoRa import * 5 | from SX127x.LoRaArgumentParser import LoRaArgumentParser 6 | from SX127x.board_config import BOARD 7 | import LoRaWAN 8 | from LoRaWAN.MHDR import MHDR 9 | 10 | BOARD.setup() 11 | parser = LoRaArgumentParser("LoRaWAN sender") 12 | 13 | class LoRaWANsend(LoRa): 14 | def __init__(self, devaddr = [], nwkey = [], appkey = [], verbose = False): 15 | super(LoRaWANsend, self).__init__(verbose) 16 | self.devaddr = devaddr 17 | self.nwkey = nwkey 18 | self.appkey = appkey 19 | 20 | def on_tx_done(self): 21 | self.set_mode(MODE.STDBY) 22 | self.clear_irq_flags(TxDone=1) 23 | print("TxDone") 24 | sys.exit(0) 25 | 26 | def start(self): 27 | lorawan = LoRaWAN.new(nwskey, appskey) 28 | lorawan.create(MHDR.UNCONF_DATA_UP, {'devaddr': devaddr, 'fcnt': 1, 'data': list(map(ord, 'Python rules!')) }) 29 | 30 | self.write_payload(lorawan.to_raw()) 31 | self.set_mode(MODE.TX) 32 | while True: 33 | sleep(1) 34 | 35 | 36 | # Init 37 | devaddr = [0x26, 0x01, 0x11, 0x5F] 38 | nwskey = [0xC3, 0x24, 0x64, 0x98, 0xDE, 0x56, 0x5D, 0x8C, 0x55, 0x88, 0x7C, 0x05, 0x86, 0xF9, 0x82, 0x26] 39 | appskey = [0x15, 0xF6, 0xF4, 0xD4, 0x2A, 0x95, 0xB0, 0x97, 0x53, 0x27, 0xB7, 0xC1, 0x45, 0x6E, 0xC5, 0x45] 40 | lora = LoRaWANsend(False) 41 | 42 | # Setup 43 | lora.set_mode(MODE.SLEEP) 44 | lora.set_dio_mapping([1,0,0,0,0,0]) 45 | lora.set_freq(868.1) 46 | lora.set_pa_config(pa_select=1) 47 | lora.set_spreading_factor(7) 48 | lora.set_pa_config(max_power=0x0F, output_power=0x0E) 49 | lora.set_sync_word(0x34) 50 | lora.set_rx_crc(True) 51 | 52 | print(lora) 53 | assert(lora.get_agc_auto_on() == 1) 54 | 55 | try: 56 | print("Sending LoRaWAN message\n") 57 | lora.start() 58 | except KeyboardInterrupt: 59 | sys.stdout.flush() 60 | print("\nKeyboardInterrupt") 61 | finally: 62 | sys.stdout.flush() 63 | lora.set_mode(MODE.SLEEP) 64 | BOARD.teardown() 65 | -------------------------------------------------------------------------------- /LoRaWAN/AES_CMAC.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from struct import pack, unpack 3 | 4 | class AES_CMAC: 5 | def gen_subkey(self, K): 6 | AES_128 = AES.new(K) 7 | 8 | L = AES_128.encrypt('\x00'*16) 9 | 10 | LHigh = unpack('>Q',L[:8])[0] 11 | LLow = unpack('>Q',L[8:])[0] 12 | 13 | K1High = ((LHigh << 1) | ( LLow >> 63 )) & 0xFFFFFFFFFFFFFFFF 14 | K1Low = (LLow << 1) & 0xFFFFFFFFFFFFFFFF 15 | 16 | if (LHigh >> 63): 17 | K1Low ^= 0x87 18 | 19 | K2High = ((K1High << 1) | (K1Low >> 63)) & 0xFFFFFFFFFFFFFFFF 20 | K2Low = ((K1Low << 1)) & 0xFFFFFFFFFFFFFFFF 21 | 22 | if (K1High >> 63): 23 | K2Low ^= 0x87 24 | 25 | K1 = pack('>QQ', K1High, K1Low) 26 | K2 = pack('>QQ', K2High, K2Low) 27 | 28 | return K1, K2 29 | 30 | def xor_128(self, N1, N2): 31 | J = b'' 32 | for i in range(len(N1)): 33 | J += bytes([N1[i] ^ N2[i]]) 34 | return J 35 | 36 | def pad(self, N): 37 | const_Bsize = 16 38 | padLen = 16-len(N) 39 | return N + b'\x80' + b'\x00'*(padLen-1) 40 | 41 | def encode(self, K, M): 42 | const_Bsize = 16 43 | const_Zero = b'\x00'*16 44 | 45 | AES_128= AES.new(K) 46 | K1, K2 = self.gen_subkey(K) 47 | n = int(len(M)/const_Bsize) 48 | 49 | if n == 0: 50 | n = 1 51 | flag = False 52 | else: 53 | if (len(M) % const_Bsize) == 0: 54 | flag = True 55 | else: 56 | n += 1 57 | flag = False 58 | 59 | M_n = M[(n-1)*const_Bsize:] 60 | if flag is True: 61 | M_last = self.xor_128(M_n,K1) 62 | else: 63 | M_last = self.xor_128(self.pad(M_n),K2) 64 | 65 | X = const_Zero 66 | for i in range(n-1): 67 | M_i = M[(i)*const_Bsize:][:16] 68 | Y = self.xor_128(X, M_i) 69 | X = AES_128.encrypt(Y) 70 | Y = self.xor_128(M_last, X) 71 | T = AES_128.encrypt(Y) 72 | 73 | return T 74 | -------------------------------------------------------------------------------- /rx_ttn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from time import sleep 3 | from SX127x.LoRa import * 4 | from SX127x.LoRaArgumentParser import LoRaArgumentParser 5 | from SX127x.board_config import BOARD 6 | import LoRaWAN 7 | 8 | BOARD.setup() 9 | parser = LoRaArgumentParser("LoRaWAN receiver") 10 | 11 | class LoRaWANrcv(LoRa): 12 | def __init__(self, verbose = False): 13 | super(LoRaWANrcv, self).__init__(verbose) 14 | 15 | def on_rx_done(self): 16 | print("RxDone") 17 | 18 | self.clear_irq_flags(RxDone=1) 19 | payload = self.read_payload(nocheck=True) 20 | print("".join(format(x, '02x') for x in bytes(payload))) 21 | 22 | lorawan = LoRaWAN.new(nwskey, appskey) 23 | lorawan.read(payload) 24 | print(lorawan.get_mhdr().get_mversion()) 25 | print(lorawan.get_mhdr().get_mtype()) 26 | print(lorawan.get_mic()) 27 | print(lorawan.compute_mic()) 28 | print(lorawan.valid_mic()) 29 | print("".join(list(map(chr, lorawan.get_payload())))) 30 | print("\n") 31 | 32 | self.set_mode(MODE.SLEEP) 33 | self.reset_ptr_rx() 34 | self.set_mode(MODE.RXCONT) 35 | 36 | def start(self): 37 | self.reset_ptr_rx() 38 | self.set_mode(MODE.RXCONT) 39 | while True: 40 | sleep(.5) 41 | 42 | 43 | # Init 44 | nwskey = [0xC3, 0x24, 0x64, 0x98, 0xDE, 0x56, 0x5D, 0x8C, 0x55, 0x88, 0x7C, 0x05, 0x86, 0xF9, 0x82, 0x26] 45 | appskey = [0x15, 0xF6, 0xF4, 0xD4, 0x2A, 0x95, 0xB0, 0x97, 0x53, 0x27, 0xB7, 0xC1, 0x45, 0x6E, 0xC5, 0x45] 46 | lora = LoRaWANrcv(False) 47 | 48 | # Setup 49 | lora.set_mode(MODE.SLEEP) 50 | lora.set_dio_mapping([0] * 6) 51 | lora.set_freq(868.1) 52 | lora.set_pa_config(pa_select=1) 53 | lora.set_spreading_factor(7) 54 | lora.set_sync_word(0x34) 55 | lora.set_rx_crc(True) 56 | 57 | print(lora) 58 | assert(lora.get_agc_auto_on() == 1) 59 | 60 | try: 61 | print("Waiting for incoming LoRaWAN messages\n") 62 | lora.start() 63 | except KeyboardInterrupt: 64 | sys.stdout.flush() 65 | print("\nKeyboardInterrupt") 66 | finally: 67 | sys.stdout.flush() 68 | lora.set_mode(MODE.SLEEP) 69 | BOARD.teardown() 70 | -------------------------------------------------------------------------------- /otaa_ttn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | from time import sleep 4 | from SX127x.LoRa import * 5 | from SX127x.LoRaArgumentParser import LoRaArgumentParser 6 | from SX127x.board_config import BOARD 7 | import LoRaWAN 8 | from LoRaWAN.MHDR import MHDR 9 | from random import randrange 10 | 11 | BOARD.setup() 12 | parser = LoRaArgumentParser("LoRaWAN sender") 13 | 14 | class LoRaWANotaa(LoRa): 15 | def __init__(self, verbose = False): 16 | super(LoRaWANotaa, self).__init__(verbose) 17 | 18 | def on_rx_done(self): 19 | print("RxDone") 20 | 21 | self.clear_irq_flags(RxDone=1) 22 | payload = self.read_payload(nocheck=True) 23 | 24 | lorawan = LoRaWAN.new([], appkey) 25 | lorawan.read(payload) 26 | print(lorawan.get_payload()) 27 | print(lorawan.get_mhdr().get_mversion()) 28 | 29 | if lorawan.get_mhdr().get_mtype() == MHDR.JOIN_ACCEPT: 30 | print("Got LoRaWAN join accept") 31 | print(lorawan.valid_mic()) 32 | print(lorawan.get_devaddr()) 33 | print(lorawan.derive_nwskey(devnonce)) 34 | print(lorawan.derive_appskey(devnonce)) 35 | print("\n") 36 | sys.exit(0) 37 | 38 | print("Got LoRaWAN message continue listen for join accept") 39 | 40 | def on_tx_done(self): 41 | self.clear_irq_flags(TxDone=1) 42 | print("TxDone") 43 | 44 | self.set_mode(MODE.STDBY) 45 | self.set_dio_mapping([0,0,0,0,0,0]) 46 | self.set_invert_iq(1) 47 | self.reset_ptr_rx() 48 | self.set_mode(MODE.RXCONT) 49 | 50 | def start(self): 51 | self.tx_counter = 1 52 | 53 | lorawan = LoRaWAN.new(appkey) 54 | lorawan.create(MHDR.JOIN_REQUEST, {'deveui': deveui, 'appeui': appeui, 'devnonce': devnonce}) 55 | 56 | self.write_payload(lorawan.to_raw()) 57 | self.set_mode(MODE.TX) 58 | while True: 59 | sleep(1) 60 | 61 | 62 | # Init 63 | deveui = [0x00, 0x47, 0x64, 0xB1, 0xAB, 0xC6, 0x4F, 0x7C] 64 | appeui = [0x70, 0xB3, 0xD5, 0x7E, 0xF0, 0x00, 0x51, 0x34] 65 | appkey = [0xA1, 0x0F, 0x0E, 0x87, 0x0A, 0x15, 0x58, 0x40, 0x89, 0x73, 0xC0, 0x60, 0x1E, 0x19, 0xC3, 0xD1] 66 | devnonce = [randrange(256), randrange(256)] 67 | lora = LoRaWANotaa(False) 68 | 69 | # Setup 70 | lora.set_mode(MODE.SLEEP) 71 | lora.set_dio_mapping([1,0,0,0,0,0]) 72 | lora.set_freq(868.1) 73 | lora.set_pa_config(pa_select=1) 74 | lora.set_spreading_factor(7) 75 | lora.set_pa_config(max_power=0x0F, output_power=0x0E) 76 | lora.set_sync_word(0x34) 77 | lora.set_rx_crc(True) 78 | 79 | print(lora) 80 | assert(lora.get_agc_auto_on() == 1) 81 | 82 | try: 83 | print("Sending LoRaWAN join request\n") 84 | lora.start() 85 | except KeyboardInterrupt: 86 | sys.stdout.flush() 87 | print("\nKeyboardInterrupt") 88 | finally: 89 | sys.stdout.flush() 90 | lora.set_mode(MODE.SLEEP) 91 | BOARD.teardown() 92 | -------------------------------------------------------------------------------- /LoRaWAN/MacPayload.py: -------------------------------------------------------------------------------- 1 | # 2 | # mac_payload: fhdr(7..23) fport(1) frm_payload(0..N) 3 | # 4 | from .MalformedPacketException import MalformedPacketException 5 | from .FHDR import FHDR 6 | from .MHDR import MHDR 7 | from .JoinRequestPayload import JoinRequestPayload 8 | from .JoinAcceptPayload import JoinAcceptPayload 9 | from .DataPayload import DataPayload 10 | 11 | class MacPayload: 12 | 13 | def read(self, mtype, mac_payload): 14 | if len(mac_payload) < 1: 15 | raise MalformedPacketException("Invalid mac payload") 16 | 17 | self.fhdr = FHDR() 18 | self.fhdr.read(mac_payload) 19 | self.fport = mac_payload[self.fhdr.length()] 20 | self.frm_payload = None 21 | if mtype == MHDR.JOIN_REQUEST: 22 | self.frm_payload = JoinRequestPayload() 23 | self.frm_payload.read(mac_payload) 24 | if mtype == MHDR.JOIN_ACCEPT: 25 | self.frm_payload = JoinAcceptPayload() 26 | self.frm_payload.read(mac_payload) 27 | if mtype == MHDR.UNCONF_DATA_UP or mtype == MHDR.UNCONF_DATA_DOWN or\ 28 | mtype == MHDR.CONF_DATA_UP or mtype == MHDR.CONF_DATA_DOWN: 29 | self.frm_payload = DataPayload() 30 | self.frm_payload.read(self, mac_payload[self.fhdr.length() + 1:]) 31 | 32 | def create(self, mtype, key, args): 33 | self.fhdr = FHDR() 34 | self.fhdr.create(mtype, args) 35 | self.fport = 0x01 36 | self.frm_payload = None 37 | if mtype == MHDR.JOIN_REQUEST: 38 | self.frm_payload = JoinRequestPayload() 39 | self.frm_payload.create(args) 40 | if mtype == MHDR.JOIN_ACCEPT: 41 | self.frm_payload = JoinAcceptPayload() 42 | self.frm_payload.create(args) 43 | if mtype == MHDR.UNCONF_DATA_UP or mtype == MHDR.UNCONF_DATA_DOWN or\ 44 | mtype == MHDR.CONF_DATA_UP or mtype == MHDR.CONF_DATA_DOWN: 45 | self.frm_payload = DataPayload() 46 | self.frm_payload.create(self, key, args) 47 | 48 | def length(self): 49 | return len(self.to_raw()) 50 | 51 | def to_raw(self): 52 | mac_payload = [] 53 | if self.fhdr.get_devaddr() != [0x00, 0x00, 0x00, 0x00]: 54 | mac_payload += self.fhdr.to_raw() 55 | if self.frm_payload != None: 56 | if self.fhdr.get_devaddr() != [0x00, 0x00, 0x00, 0x00]: 57 | mac_payload += [self.fport] 58 | mac_payload += self.frm_payload.to_raw() 59 | return mac_payload 60 | 61 | def get_fhdr(self): 62 | return self.fhdr 63 | 64 | def set_fhdr(self, fhdr): 65 | self.fhdr = fhdr 66 | 67 | def get_fport(self): 68 | return self.fport 69 | 70 | def set_fport(self, fport): 71 | self.fport = fport 72 | 73 | def get_frm_payload(self): 74 | return self.frm_payload 75 | 76 | def set_frm_payload(self, frm_payload): 77 | self.frm_payload = frm_payload 78 | -------------------------------------------------------------------------------- /LoRaWAN/JoinAcceptPayload.py: -------------------------------------------------------------------------------- 1 | # 2 | # frm_payload: appnonce(3) netid(3) devaddr(4) dlsettings(1) rxdelay(1) cflist(0..16) 3 | # 4 | from .MalformedPacketException import MalformedPacketException 5 | from .AES_CMAC import AES_CMAC 6 | from Crypto.Cipher import AES 7 | 8 | class JoinAcceptPayload: 9 | 10 | def read(self, payload): 11 | if len(payload) < 12: 12 | raise MalformedPacketException("Invalid join accept"); 13 | self.encrypted_payload = payload 14 | 15 | def create(self, args): 16 | pass 17 | 18 | def length(self): 19 | return len(self.encrypted_payload) 20 | 21 | def to_raw(self): 22 | return self.encrypted_payload 23 | 24 | def to_clear_raw(self): 25 | return self.payload 26 | 27 | def get_appnonce(self): 28 | return self.appnonce 29 | 30 | def get_netid(self): 31 | return self.netid 32 | 33 | def get_devaddr(self): 34 | return list(map(int, reversed(self.devaddr))) 35 | 36 | def get_dlsettings(self): 37 | return self.dlsettings 38 | 39 | def get_rxdelay(self): 40 | return self.rxdelay 41 | 42 | def get_cflist(self): 43 | return self.cflist 44 | 45 | def compute_mic(self, key, direction, mhdr): 46 | mic = [] 47 | mic += [mhdr.to_raw()] 48 | mic += self.to_clear_raw() 49 | 50 | cmac = AES_CMAC() 51 | computed_mic = cmac.encode(bytes(key), bytes(mic))[:4] 52 | return list(map(int, computed_mic)) 53 | 54 | def decrypt_payload(self, key, direction, mic): 55 | a = [] 56 | a += self.encrypted_payload 57 | a += mic 58 | 59 | cipher = AES.new(bytes(key)) 60 | self.payload = cipher.encrypt(bytes(a))[:-4] 61 | 62 | self.appnonce = self.payload[:3] 63 | self.netid = self.payload[3:6] 64 | self.devaddr = self.payload[6:10] 65 | self.dlsettings = self.payload[10] 66 | self.rxdelay = self.payload[11] 67 | self.cflist = None 68 | if self.payload[12:]: 69 | self.cflist = self.payload[12:] 70 | 71 | return list(map(int, self.payload)) 72 | 73 | def encrypt_payload(self, key, direction, mhdr): 74 | a = [] 75 | a += self.to_clear_raw() 76 | a += self.compute_mic(key, direction, mhdr) 77 | 78 | cipher = AES.new(bytes(key)) 79 | return list(map(int, cipher.decrypt(bytes(a)))) 80 | 81 | def derive_nwskey(self, key, devnonce): 82 | a = [0x01] 83 | a += self.get_appnonce() 84 | a += self.get_netid() 85 | a += devnonce 86 | a += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 87 | 88 | cipher = AES.new(bytes(key)) 89 | return list(map(int, cipher.encrypt(bytes(a)))) 90 | 91 | def derive_appskey(self, key, devnonce): 92 | a = [0x02] 93 | a += self.get_appnonce() 94 | a += self.get_netid() 95 | a += devnonce 96 | a += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] 97 | 98 | cipher = AES.new(bytes(key)) 99 | return list(map(int, cipher.encrypt(bytes(a)))) 100 | -------------------------------------------------------------------------------- /LoRaWAN/DataPayload.py: -------------------------------------------------------------------------------- 1 | # 2 | # frm_payload: data(0..N) 3 | # 4 | from .AES_CMAC import AES_CMAC 5 | from Crypto.Cipher import AES 6 | import math 7 | 8 | class DataPayload: 9 | 10 | def read(self, mac_payload, payload): 11 | self.mac_payload = mac_payload 12 | self.payload = payload 13 | 14 | def create(self, mac_payload, key, args): 15 | self.mac_payload = mac_payload 16 | self.set_payload(key, 0x00, args['data']) 17 | 18 | def length(self): 19 | return len(self.payload) 20 | 21 | def to_raw(self): 22 | return self.payload 23 | 24 | def set_payload(self, key, direction, data): 25 | self.payload = self.encrypt_payload(key, direction, data) 26 | 27 | def compute_mic(self, key, direction, mhdr): 28 | mic = [0x49] 29 | mic += [0x00, 0x00, 0x00, 0x00] 30 | mic += [direction] 31 | mic += self.mac_payload.get_fhdr().get_devaddr() 32 | mic += self.mac_payload.get_fhdr().get_fcnt() 33 | mic += [0x00] 34 | mic += [0x00] 35 | mic += [0x00] 36 | mic += [1 + self.mac_payload.length()] 37 | mic += [mhdr.to_raw()] 38 | mic += self.mac_payload.to_raw() 39 | 40 | cmac = AES_CMAC() 41 | computed_mic = cmac.encode(bytes(key), bytes(mic))[:4] 42 | return list(map(int, computed_mic)) 43 | 44 | def decrypt_payload(self, key, direction, mic): 45 | k = int(math.ceil(len(self.payload) / 16.0)) 46 | 47 | a = [] 48 | for i in range(k): 49 | a += [0x01] 50 | a += [0x00, 0x00, 0x00, 0x00] 51 | a += [direction] 52 | a += self.mac_payload.get_fhdr().get_devaddr() 53 | a += self.mac_payload.get_fhdr().get_fcnt() 54 | a += [0x00] # fcnt 32bit 55 | a += [0x00] # fcnt 32bit 56 | a += [0x00] 57 | a += [i+1] 58 | 59 | cipher = AES.new(bytes(key)) 60 | s = cipher.encrypt(bytes(a)) 61 | 62 | padded_payload = [] 63 | for i in range(k): 64 | idx = (i + 1) * 16 65 | padded_payload += (self.payload[idx - 16:idx] + ([0x00] * 16))[:16] 66 | 67 | payload = [] 68 | for i in range(len(self.payload)): 69 | payload += [s[i] ^ padded_payload[i]] 70 | return list(map(int, payload)) 71 | 72 | def encrypt_payload(self, key, direction, data): 73 | k = int(math.ceil(len(data) / 16.0)) 74 | 75 | a = [] 76 | for i in range(k): 77 | a += [0x01] 78 | a += [0x00, 0x00, 0x00, 0x00] 79 | a += [direction] 80 | a += self.mac_payload.get_fhdr().get_devaddr() 81 | a += self.mac_payload.get_fhdr().get_fcnt() 82 | a += [0x00] # fcnt 32bit 83 | a += [0x00] # fcnt 32bit 84 | a += [0x00] 85 | a += [i+1] 86 | 87 | cipher = AES.new(bytes(key)) 88 | s = cipher.encrypt(bytes(a)) 89 | 90 | padded_payload = [] 91 | for i in range(k): 92 | idx = (i + 1) * 16 93 | padded_payload += (data[idx - 16:idx] + ([0x00] * 16))[:16] 94 | 95 | payload = [] 96 | for i in range(len(data)): 97 | payload += [s[i] ^ padded_payload[i]] 98 | return list(map(int, payload)) 99 | -------------------------------------------------------------------------------- /LoRaWAN/PhyPayload.py: -------------------------------------------------------------------------------- 1 | # 2 | # lorawan packet: mhdr(1) mac_payload(1..N) mic(4) 3 | # 4 | from .MalformedPacketException import MalformedPacketException 5 | from .MHDR import MHDR 6 | from .Direction import Direction 7 | from .MacPayload import MacPayload 8 | 9 | class PhyPayload: 10 | 11 | def __init__(self, nwkey, appkey): 12 | self.nwkey = nwkey 13 | self.appkey = appkey 14 | 15 | def read(self, packet): 16 | if len(packet) < 12: 17 | raise MalformedPacketException("Invalid lorawan packet"); 18 | 19 | self.mhdr = MHDR(packet[0]) 20 | self.set_direction() 21 | self.mac_payload = MacPayload() 22 | self.mac_payload.read(self.get_mhdr().get_mtype(), packet[1:-4]) 23 | self.mic = packet[-4:] 24 | 25 | def create(self, mhdr, args): 26 | self.mhdr = MHDR(mhdr) 27 | self.set_direction() 28 | self.mac_payload = MacPayload() 29 | self.mac_payload.create(self.get_mhdr().get_mtype(), self.appkey, args) 30 | self.mic = None 31 | 32 | def length(self): 33 | return len(self.to_raw()) 34 | 35 | def to_raw(self): 36 | phy_payload = [self.get_mhdr().to_raw()] 37 | phy_payload += self.mac_payload.to_raw() 38 | phy_payload += self.get_mic() 39 | return phy_payload 40 | 41 | def get_mhdr(self): 42 | return self.mhdr; 43 | 44 | def set_mhdr(self, mhdr): 45 | self.mhdr = mhdr 46 | 47 | def get_direction(self): 48 | return self.direction.get() 49 | 50 | def set_direction(self): 51 | self.direction = Direction(self.get_mhdr()) 52 | 53 | def get_mac_payload(self): 54 | return self.mac_payload 55 | 56 | def set_mac_payload(self, mac_payload): 57 | self.mac_payload = mac_payload 58 | 59 | def get_mic(self): 60 | if self.mic == None: 61 | self.set_mic(self.compute_mic()) 62 | return self.mic 63 | 64 | def set_mic(self, mic): 65 | self.mic = mic 66 | 67 | def compute_mic(self): 68 | if self.get_mhdr().get_mtype() == MHDR.JOIN_ACCEPT: 69 | return self.mac_payload.frm_payload.encrypt_payload(self.appkey, self.get_direction(), self.get_mhdr())[-4:] 70 | else: 71 | return self.mac_payload.frm_payload.compute_mic(self.nwkey, self.get_direction(), self.get_mhdr()) 72 | 73 | def valid_mic(self): 74 | if self.get_mhdr().get_mtype() == MHDR.JOIN_ACCEPT: 75 | return self.get_mic() == self.mac_payload.frm_payload.encrypt_payload(self.appkey, self.get_direction(), self.get_mhdr())[-4:] 76 | else: 77 | return self.get_mic() == self.mac_payload.frm_payload.compute_mic(self.nwkey, self.get_direction(), self.get_mhdr()) 78 | 79 | def get_devaddr(self): 80 | if self.get_mhdr().get_mtype() == MHDR.JOIN_ACCEPT: 81 | return self.mac_payload.frm_payload.get_devaddr() 82 | else: 83 | return self.mac_payload.fhdr.get_devaddr() 84 | 85 | def get_payload(self): 86 | return self.mac_payload.frm_payload.decrypt_payload(self.appkey, self.get_direction(), self.mic) 87 | 88 | def derive_nwskey(self, devnonce): 89 | return self.mac_payload.frm_payload.derive_nwskey(self.appkey, devnonce) 90 | 91 | def derive_appskey(self, devnonce): 92 | return self.mac_payload.frm_payload.derive_appskey(self.appkey, devnonce) 93 | -------------------------------------------------------------------------------- /SX127x/LoRaArgumentParser.py: -------------------------------------------------------------------------------- 1 | """ Defines LoRaArgumentParser which extends argparse.ArgumentParser with standard config parameters for the SX127x. """ 2 | 3 | # Copyright 2015 Mayer Analytics Ltd. 4 | # 5 | # This file is part of pySX127x. 6 | # 7 | # pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public 8 | # License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later 9 | # version. 10 | # 11 | # pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 12 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 13 | # details. 14 | # 15 | # You can be released from the requirements of the license by obtaining a commercial license. Such a license is 16 | # mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your 17 | # own applications, or shipping pySX127x with a closed source product. 18 | # 19 | # You should have received a copy of the GNU General Public License along with pySX127. If not, see 20 | # . 21 | 22 | 23 | import argparse 24 | 25 | 26 | class LoRaArgumentParser(argparse.ArgumentParser): 27 | """ This class extends argparse.ArgumentParser. 28 | Some commonly used LoRa config parameters are defined 29 | * ocp 30 | * spreading factor 31 | * frequency 32 | * bandwidth 33 | * preamble 34 | Call the parse_args with an additional parameter referencing a LoRa object. The args will be used to configure 35 | the LoRa. 36 | """ 37 | 38 | bw_lookup = dict(BW7_8=0, BW10_4=1, BW15_6=2, BW20_8=3, BW31_25=4, BW41_7=5, BW62_5=6, BW125=7, BW250=8, BW500=9) 39 | cr_lookup = dict(CR4_5=1, CR4_6=2,CR4_7=3,CR4_8=4) 40 | 41 | def __init__(self, description): 42 | argparse.ArgumentParser.__init__(self, description=description) 43 | self.add_argument('--ocp', '-c', dest='ocp', default=100, action="store", type=float, 44 | help="Over current protection in mA (45 .. 240 mA)") 45 | self.add_argument('--sf', '-s', dest='sf', default=7, action="store", type=int, 46 | help="Spreading factor (6...12). Default is 7.") 47 | self.add_argument('--freq', '-f', dest='freq', default=869., action="store", type=float, 48 | help="Frequency") 49 | self.add_argument('--bw', '-b', dest='bw', default='BW125', action="store", type=str, 50 | help="Bandwidth (one of BW7_8 BW10_4 BW15_6 BW20_8 BW31_25 BW41_7 BW62_5 BW125 BW250 BW500).\nDefault is BW125.") 51 | self.add_argument('--cr', '-r', dest='coding_rate', default='CR4_5', action="store", type=str, 52 | help="Coding rate (one of CR4_5 CR4_6 CR4_7 CR4_8).\nDefault is CR4_5.") 53 | self.add_argument('--preamble', '-p', dest='preamble', default=8, action="store", type=int, 54 | help="Preamble length. Default is 8.") 55 | 56 | def parse_args(self, lora): 57 | """ Parse the args, perform some sanity checks and configure the LoRa accordingly. 58 | :param lora: Reference to LoRa object 59 | :return: args 60 | """ 61 | args = argparse.ArgumentParser.parse_args(self) 62 | args.bw = self.bw_lookup.get(args.bw, None) 63 | args.coding_rate = self.cr_lookup.get(args.coding_rate, None) 64 | # some sanity checks 65 | assert(args.bw is not None) 66 | assert(args.coding_rate is not None) 67 | assert(args.sf >=6 and args.sf <= 12) 68 | # set the LoRa object 69 | lora.set_freq(args.freq) 70 | lora.set_preamble(args.preamble) 71 | lora.set_spreading_factor(args.sf) 72 | lora.set_bw(args.bw) 73 | lora.set_coding_rate(args.coding_rate) 74 | lora.set_ocp_trim(args.ocp) 75 | return args 76 | -------------------------------------------------------------------------------- /SX127x/board_config.py: -------------------------------------------------------------------------------- 1 | """ Defines the BOARD class that contains the board pin mappings. """ 2 | 3 | # Copyright 2015 Mayer Analytics Ltd. 4 | # 5 | # This file is part of pySX127x. 6 | # 7 | # pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public 8 | # License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later 9 | # version. 10 | # 11 | # pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 12 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 13 | # details. 14 | # 15 | # You can be released from the requirements of the license by obtaining a commercial license. Such a license is 16 | # mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your 17 | # own applications, or shipping pySX127x with a closed source product. 18 | # 19 | # You should have received a copy of the GNU General Public License along with pySX127. If not, see 20 | # . 21 | 22 | 23 | import RPi.GPIO as GPIO 24 | import spidev 25 | 26 | import time 27 | 28 | 29 | class BOARD: 30 | """ Board initialisation/teardown and pin configuration is kept here. 31 | This is the Raspberry Pi board with one LED and a modtronix inAir9B 32 | """ 33 | # Note that the BCOM numbering for the GPIOs is used. 34 | DIO0 = 22 # RaspPi GPIO 22 35 | DIO1 = 23 # RaspPi GPIO 23 36 | DIO2 = 24 # RaspPi GPIO 24 37 | DIO3 = 25 # RaspPi GPIO 25 38 | LED = 18 # RaspPi GPIO 18 connects to the LED on the proto shield 39 | SWITCH = 4 # RaspPi GPIO 4 connects to a switch 40 | 41 | # The spi object is kept here 42 | spi = None 43 | 44 | @staticmethod 45 | def setup(): 46 | """ Configure the Raspberry GPIOs 47 | :rtype : None 48 | """ 49 | GPIO.setmode(GPIO.BCM) 50 | # LED 51 | GPIO.setup(BOARD.LED, GPIO.OUT) 52 | GPIO.output(BOARD.LED, 0) 53 | # switch 54 | GPIO.setup(BOARD.SWITCH, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 55 | # DIOx 56 | for gpio_pin in [BOARD.DIO0, BOARD.DIO1, BOARD.DIO2, BOARD.DIO3]: 57 | GPIO.setup(gpio_pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 58 | # blink 2 times to signal the board is set up 59 | BOARD.blink(.1, 2) 60 | 61 | @staticmethod 62 | def teardown(): 63 | """ Cleanup GPIO and SpiDev """ 64 | GPIO.cleanup() 65 | BOARD.spi.close() 66 | 67 | @staticmethod 68 | def SpiDev(spi_bus=0, spi_cs=0): 69 | """ Init and return the SpiDev object 70 | :return: SpiDev object 71 | :param spi_bus: The RPi SPI bus to use: 0 or 1 72 | :param spi_cs: The RPi SPI chip select to use: 0 or 1 73 | :rtype: SpiDev 74 | """ 75 | BOARD.spi = spidev.SpiDev() 76 | BOARD.spi.open(spi_bus, spi_cs) 77 | return BOARD.spi 78 | 79 | @staticmethod 80 | def add_event_detect(dio_number, callback): 81 | """ Wraps around the GPIO.add_event_detect function 82 | :param dio_number: DIO pin 0...5 83 | :param callback: The function to call when the DIO triggers an IRQ. 84 | :return: None 85 | """ 86 | GPIO.add_event_detect(dio_number, GPIO.RISING, callback=callback) 87 | 88 | @staticmethod 89 | def add_events(cb_dio0, cb_dio1, cb_dio2, cb_dio3, cb_dio4, cb_dio5, switch_cb=None): 90 | BOARD.add_event_detect(BOARD.DIO0, callback=cb_dio0) 91 | BOARD.add_event_detect(BOARD.DIO1, callback=cb_dio1) 92 | BOARD.add_event_detect(BOARD.DIO2, callback=cb_dio2) 93 | BOARD.add_event_detect(BOARD.DIO3, callback=cb_dio3) 94 | # the modtronix inAir9B does not expose DIO4 and DIO5 95 | if switch_cb is not None: 96 | GPIO.add_event_detect(BOARD.SWITCH, GPIO.RISING, callback=switch_cb, bouncetime=300) 97 | 98 | @staticmethod 99 | def led_on(value=1): 100 | """ Switch the proto shields LED 101 | :param value: 0/1 for off/on. Default is 1. 102 | :return: value 103 | :rtype : int 104 | """ 105 | GPIO.output(BOARD.LED, value) 106 | return value 107 | 108 | @staticmethod 109 | def led_off(): 110 | """ Switch LED off 111 | :return: 0 112 | """ 113 | GPIO.output(BOARD.LED, 0) 114 | return 0 115 | 116 | @staticmethod 117 | def blink(time_sec, n_blink): 118 | if n_blink == 0: 119 | return 120 | BOARD.led_on() 121 | for i in range(n_blink): 122 | time.sleep(time_sec) 123 | BOARD.led_off() 124 | time.sleep(time_sec) 125 | BOARD.led_on() 126 | BOARD.led_off() 127 | -------------------------------------------------------------------------------- /SX127x/constants.py: -------------------------------------------------------------------------------- 1 | """ Defines constants (modes, bandwidths, registers, etc.) needed by SX127x. """ 2 | 3 | 4 | # Copyright 2015 Mayer Analytics Ltd. 5 | # 6 | # This file is part of pySX127x. 7 | # 8 | # pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public 9 | # License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later 10 | # version. 11 | # 12 | # pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 13 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 14 | # details. 15 | # 16 | # You can be released from the requirements of the license by obtaining a commercial license. Such a license is 17 | # mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your 18 | # own applications, or shipping pySX127x with a closed source product. 19 | # 20 | # You should have received a copy of the GNU General Public License along with pySX127. If not, see 21 | # . 22 | 23 | 24 | def add_lookup(cls): 25 | """ A decorator that adds a lookup dictionary to the class. 26 | The lookup dictionary maps the codes back to the names. This is used for pretty-printing. """ 27 | varnames = filter(str.isupper, cls.__dict__.keys()) 28 | lookup = dict(map(lambda varname: (cls.__dict__.get(varname, None), varname), varnames)) 29 | setattr(cls, 'lookup', lookup) 30 | return cls 31 | 32 | 33 | @add_lookup 34 | class MODE: 35 | SLEEP = 0x80 36 | STDBY = 0x81 37 | FSTX = 0x82 38 | TX = 0x83 39 | FSRX = 0x84 40 | RXCONT = 0x85 41 | RXSINGLE = 0x86 42 | CAD = 0x87 43 | FSK_STDBY= 0x01 # needed for calibration 44 | 45 | 46 | @add_lookup 47 | class BW: 48 | BW7_8 = 0 49 | BW10_4 = 1 50 | BW15_6 = 2 51 | BW20_8 = 3 52 | BW31_25 = 4 53 | BW41_7 = 5 54 | BW62_5 = 6 55 | BW125 = 7 56 | BW250 = 8 57 | BW500 = 9 58 | 59 | 60 | @add_lookup 61 | class CODING_RATE: 62 | CR4_5 = 1 63 | CR4_6 = 2 64 | CR4_7 = 3 65 | CR4_8 = 4 66 | 67 | 68 | @add_lookup 69 | class GAIN: 70 | NOT_USED = 0b000 71 | G1 = 0b001 72 | G2 = 0b010 73 | G3 = 0b011 74 | G4 = 0b100 75 | G5 = 0b101 76 | G6 = 0b110 77 | 78 | 79 | @add_lookup 80 | class PA_SELECT: 81 | RFO = 0 82 | PA_BOOST = 1 83 | 84 | 85 | @add_lookup 86 | class PA_RAMP: 87 | RAMP_3_4_ms = 0 88 | RAMP_2_ms = 1 89 | RAMP_1_ms = 2 90 | RAMP_500_us = 3 91 | RAMP_250_us = 4 92 | RAMP_125_us = 5 93 | RAMP_100_us = 6 94 | RAMP_62_us = 7 95 | RAMP_50_us = 8 96 | RAMP_40_us = 9 97 | RAMP_31_us = 10 98 | RAMP_25_us = 11 99 | RAMP_20_us = 12 100 | RAMP_15_us = 13 101 | RAMP_12_us = 14 102 | RAMP_10_us = 15 103 | 104 | 105 | class MASK: 106 | class IRQ_FLAGS: 107 | RxTimeout = 7 108 | RxDone = 6 109 | PayloadCrcError = 5 110 | ValidHeader = 4 111 | TxDone = 3 112 | CadDone = 2 113 | FhssChangeChannel = 1 114 | CadDetected = 0 115 | 116 | 117 | class REG: 118 | 119 | @add_lookup 120 | class LORA: 121 | FIFO = 0x00 122 | OP_MODE = 0x01 123 | FR_MSB = 0x06 124 | FR_MID = 0x07 125 | FR_LSB = 0x08 126 | PA_CONFIG = 0x09 127 | PA_RAMP = 0x0A 128 | OCP = 0x0B 129 | LNA = 0x0C 130 | FIFO_ADDR_PTR = 0x0D 131 | FIFO_TX_BASE_ADDR = 0x0E 132 | FIFO_RX_BASE_ADDR = 0x0F 133 | FIFO_RX_CURR_ADDR = 0x10 134 | IRQ_FLAGS_MASK = 0x11 135 | IRQ_FLAGS = 0x12 136 | RX_NB_BYTES = 0x13 137 | RX_HEADER_CNT_MSB = 0x14 138 | RX_PACKET_CNT_MSB = 0x16 139 | MODEM_STAT = 0x18 140 | PKT_SNR_VALUE = 0x19 141 | PKT_RSSI_VALUE = 0x1A 142 | RSSI_VALUE = 0x1B 143 | HOP_CHANNEL = 0x1C 144 | MODEM_CONFIG_1 = 0x1D 145 | MODEM_CONFIG_2 = 0x1E 146 | SYMB_TIMEOUT_LSB = 0x1F 147 | PREAMBLE_MSB = 0x20 148 | PAYLOAD_LENGTH = 0x22 149 | MAX_PAYLOAD_LENGTH = 0x23 150 | HOP_PERIOD = 0x24 151 | FIFO_RX_BYTE_ADDR = 0x25 152 | MODEM_CONFIG_3 = 0x26 153 | PPM_CORRECTION = 0x27 154 | FEI_MSB = 0x28 155 | DETECT_OPTIMIZE = 0X31 156 | INVERT_IQ = 0x33 157 | DETECTION_THRESH = 0X37 158 | SYNC_WORD = 0X39 159 | DIO_MAPPING_1 = 0x40 160 | DIO_MAPPING_2 = 0x41 161 | VERSION = 0x42 162 | TCXO = 0x4B 163 | PA_DAC = 0x4D 164 | AGC_REF = 0x61 165 | AGC_THRESH_1 = 0x62 166 | AGC_THRESH_2 = 0x63 167 | AGC_THRESH_3 = 0x64 168 | PLL = 0x70 169 | 170 | @add_lookup 171 | class FSK: 172 | LNA = 0x0C 173 | RX_CONFIG = 0x0D 174 | RSSI_CONFIG = 0x0E 175 | PREAMBLE_DETECT = 0x1F 176 | OSC = 0x24 177 | SYNC_CONFIG = 0x27 178 | SYNC_VALUE_1 = 0x28 179 | SYNC_VALUE_2 = 0x29 180 | SYNC_VALUE_3 = 0x2A 181 | SYNC_VALUE_4 = 0x2B 182 | SYNC_VALUE_5 = 0x2C 183 | SYNC_VALUE_6 = 0x2D 184 | SYNC_VALUE_7 = 0x2E 185 | SYNC_VALUE_8 = 0x2F 186 | PACKET_CONFIG_1 = 0x30 187 | FIFO_THRESH = 0x35 188 | IMAGE_CAL = 0x3B 189 | DIO_MAPPING_1 = 0x40 190 | DIO_MAPPING_2 = 0x41 191 | -------------------------------------------------------------------------------- /SX127x/LoRa.py: -------------------------------------------------------------------------------- 1 | """ Defines the SX127x class and a few utility functions. """ 2 | 3 | # Copyright 2015 Mayer Analytics Ltd. 4 | # 5 | # This file is part of pySX127x. 6 | # 7 | # pySX127x is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public 8 | # License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later 9 | # version. 10 | # 11 | # pySX127x is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied 12 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 13 | # details. 14 | # 15 | # You can be released from the requirements of the license by obtaining a commercial license. Such a license is 16 | # mandatory as soon as you develop commercial activities involving pySX127x without disclosing the source code of your 17 | # own applications, or shipping pySX127x with a closed source product. 18 | # 19 | # You should have received a copy of the GNU General Public License along with pySX127. If not, see 20 | # . 21 | 22 | 23 | import sys 24 | from .constants import * 25 | from .board_config import BOARD 26 | 27 | 28 | ################################################## Some utility functions ############################################## 29 | 30 | def set_bit(value, index, new_bit): 31 | """ Set the index'th bit of value to new_bit, and return the new value. 32 | :param value: The integer to set the new_bit in 33 | :type value: int 34 | :param index: 0-based index 35 | :param new_bit: New value the bit shall have (0 or 1) 36 | :return: Changed value 37 | :rtype: int 38 | """ 39 | mask = 1 << index 40 | value &= ~mask 41 | if new_bit: 42 | value |= mask 43 | return value 44 | 45 | 46 | def getter(register_address): 47 | """ The getter decorator reads the register content and calls the decorated function to do 48 | post-processing. 49 | :param register_address: Register address 50 | :return: Register value 51 | :rtype: int 52 | """ 53 | def decorator(func): 54 | def wrapper(self): 55 | return func(self, self.spi.xfer([register_address, 0])[1]) 56 | return wrapper 57 | return decorator 58 | 59 | 60 | def setter(register_address): 61 | """ The setter decorator calls the decorated function for pre-processing and 62 | then writes the result to the register 63 | :param register_address: Register address 64 | :return: New register value 65 | :rtype: int 66 | """ 67 | def decorator(func): 68 | def wrapper(self, val): 69 | return self.spi.xfer([register_address | 0x80, func(self, val)])[1] 70 | return wrapper 71 | return decorator 72 | 73 | 74 | ############################################### Definition of the LoRa class ########################################### 75 | 76 | class LoRa(object): 77 | 78 | spi = BOARD.SpiDev() # init and get the baord's SPI 79 | mode = None # the mode is backed up here 80 | backup_registers = [] 81 | verbose = True 82 | dio_mapping = [None] * 6 # store the dio mapping here 83 | 84 | def __init__(self, verbose=True, do_calibration=True, calibration_freq=868): 85 | """ Init the object 86 | 87 | Send the device to sleep, read all registers, and do the calibration (if do_calibration=True) 88 | :param verbose: Set the verbosity True/False 89 | :param calibration_freq: call rx_chain_calibration with this parameter. Default is 868 90 | :param do_calibration: Call rx_chain_calibration, default is True. 91 | """ 92 | self.verbose = verbose 93 | # set the callbacks for DIO0..5 IRQs. 94 | BOARD.add_events(self._dio0, self._dio1, self._dio2, self._dio3, self._dio4, self._dio5) 95 | # set mode to sleep and read all registers 96 | self.set_mode(MODE.SLEEP) 97 | self.backup_registers = self.get_all_registers() 98 | # more setup work: 99 | if do_calibration: 100 | self.rx_chain_calibration(calibration_freq) 101 | # the FSK registers are set up exactly as modtronix do it: 102 | lookup_fsk = [ 103 | #[REG.FSK.LNA , 0x23], 104 | #[REG.FSK.RX_CONFIG , 0x1E], 105 | #[REG.FSK.RSSI_CONFIG , 0xD2], 106 | #[REG.FSK.PREAMBLE_DETECT, 0xAA], 107 | #[REG.FSK.OSC , 0x07], 108 | #[REG.FSK.SYNC_CONFIG , 0x12], 109 | #[REG.FSK.SYNC_VALUE_1 , 0xC1], 110 | #[REG.FSK.SYNC_VALUE_2 , 0x94], 111 | #[REG.FSK.SYNC_VALUE_3 , 0xC1], 112 | #[REG.FSK.PACKET_CONFIG_1, 0xD8], 113 | #[REG.FSK.FIFO_THRESH , 0x8F], 114 | #[REG.FSK.IMAGE_CAL , 0x02], 115 | #[REG.FSK.DIO_MAPPING_1 , 0x00], 116 | #[REG.FSK.DIO_MAPPING_2 , 0x30] 117 | ] 118 | self.set_mode(MODE.FSK_STDBY) 119 | for register_address, value in lookup_fsk: 120 | self.set_register(register_address, value) 121 | self.set_mode(MODE.SLEEP) 122 | # set the dio_ mapping by calling the two get_dio_mapping_* functions 123 | self.get_dio_mapping_1() 124 | self.get_dio_mapping_2() 125 | 126 | 127 | # Overridable functions: 128 | 129 | def on_rx_done(self): 130 | pass 131 | 132 | def on_tx_done(self): 133 | pass 134 | 135 | def on_cad_done(self): 136 | pass 137 | 138 | def on_rx_timeout(self): 139 | pass 140 | 141 | def on_valid_header(self): 142 | pass 143 | 144 | def on_payload_crc_error(self): 145 | pass 146 | 147 | def on_fhss_change_channel(self): 148 | pass 149 | 150 | # Internal callbacks for add_events() 151 | 152 | def _dio0(self, channel): 153 | # DIO0 00: RxDone 154 | # DIO0 01: TxDone 155 | # DIO0 10: CadDone 156 | if self.dio_mapping[0] == 0: 157 | self.on_rx_done() 158 | elif self.dio_mapping[0] == 1: 159 | self.on_tx_done() 160 | elif self.dio_mapping[0] == 2: 161 | self.on_cad_done() 162 | else: 163 | raise RuntimeError("unknown dio0mapping!") 164 | 165 | def _dio1(self, channel): 166 | # DIO1 00: RxTimeout 167 | # DIO1 01: FhssChangeChannel 168 | # DIO1 10: CadDetected 169 | if self.dio_mapping[1] == 0: 170 | self.on_rx_timeout() 171 | elif self.dio_mapping[1] == 1: 172 | self.on_fhss_change_channel() 173 | elif self.dio_mapping[1] == 2: 174 | self.on_CadDetected() 175 | else: 176 | raise RuntimeError("unknown dio1mapping!") 177 | 178 | def _dio2(self, channel): 179 | # DIO2 00: FhssChangeChannel 180 | # DIO2 01: FhssChangeChannel 181 | # DIO2 10: FhssChangeChannel 182 | self.on_fhss_change_channel() 183 | 184 | def _dio3(self, channel): 185 | # DIO3 00: CadDone 186 | # DIO3 01: ValidHeader 187 | # DIO3 10: PayloadCrcError 188 | if self.dio_mapping[3] == 0: 189 | self.on_cad_done() 190 | elif self.dio_mapping[3] == 1: 191 | self.on_valid_header() 192 | elif self.dio_mapping[3] == 2: 193 | self.on_payload_crc_error() 194 | else: 195 | raise RuntimeError("unknown dio3 mapping!") 196 | 197 | def _dio4(self, channel): 198 | raise RuntimeError("DIO4 is not used") 199 | 200 | def _dio5(self, channel): 201 | raise RuntimeError("DIO5 is not used") 202 | 203 | # All the set/get/read/write functions 204 | 205 | def get_mode(self): 206 | """ Get the mode 207 | :return: New mode 208 | """ 209 | self.mode = self.spi.xfer([REG.LORA.OP_MODE, 0])[1] 210 | return self.mode 211 | 212 | def set_mode(self, mode): 213 | """ Set the mode 214 | :param mode: Set the mode. Use constants.MODE class 215 | :return: New mode 216 | """ 217 | # the mode is backed up in self.mode 218 | if mode == self.mode: 219 | return mode 220 | if self.verbose: 221 | sys.stderr.write("Mode <- %s\n" % MODE.lookup[mode]) 222 | self.mode = mode 223 | return self.spi.xfer([REG.LORA.OP_MODE | 0x80, mode])[1] 224 | 225 | def write_payload(self, payload): 226 | """ Get FIFO ready for TX: Set FifoAddrPtr to FifoTxBaseAddr. The transceiver is put into STDBY mode. 227 | :param payload: Payload to write (list) 228 | :return: Written payload 229 | """ 230 | payload_size = len(payload) 231 | self.set_payload_length(payload_size) 232 | 233 | self.set_mode(MODE.STDBY) 234 | base_addr = self.get_fifo_tx_base_addr() 235 | self.set_fifo_addr_ptr(base_addr) 236 | return self.spi.xfer([REG.LORA.FIFO | 0x80] + payload)[1:] 237 | 238 | def reset_ptr_rx(self): 239 | """ Get FIFO ready for RX: Set FifoAddrPtr to FifoRxBaseAddr. The transceiver is put into STDBY mode. """ 240 | self.set_mode(MODE.STDBY) 241 | base_addr = self.get_fifo_rx_base_addr() 242 | self.set_fifo_addr_ptr(base_addr) 243 | 244 | def rx_is_good(self): 245 | """ Check the IRQ flags for RX errors 246 | :return: True if no errors 247 | :rtype: bool 248 | """ 249 | flags = self.get_irq_flags() 250 | return not any([flags[s] for s in ['valid_header', 'crc_error', 'rx_done', 'rx_timeout']]) 251 | 252 | def read_payload(self , nocheck = False): 253 | """ Read the payload from FIFO 254 | :param nocheck: If True then check rx_is_good() 255 | :return: Payload 256 | :rtype: list[int] 257 | """ 258 | if not nocheck and not self.rx_is_good(): 259 | return None 260 | rx_nb_bytes = self.get_rx_nb_bytes() 261 | fifo_rx_current_addr = self.get_fifo_rx_current_addr() 262 | self.set_fifo_addr_ptr(fifo_rx_current_addr) 263 | payload = self.spi.xfer([REG.LORA.FIFO] + [0] * rx_nb_bytes)[1:] 264 | return payload 265 | 266 | def get_freq(self): 267 | """ Get the frequency (MHz) 268 | :return: Frequency in MHz 269 | :rtype: float 270 | """ 271 | msb, mid, lsb = self.spi.xfer([REG.LORA.FR_MSB, 0, 0, 0])[1:] 272 | f = lsb + 256*(mid + 256*msb) 273 | return f / 16384. 274 | 275 | def set_freq(self, f): 276 | """ Set the frequency (MHz) 277 | :param f: Frequency in MHz 278 | "type f: float 279 | :return: New register settings (3 bytes [msb, mid, lsb]) 280 | :rtype: list[int] 281 | """ 282 | assert self.mode == MODE.SLEEP or self.mode == MODE.STDBY or self.mode == MODE.FSK_STDBY 283 | i = int(f * 16384.) # choose floor 284 | msb = i // 65536 285 | i -= msb * 65536 286 | mid = i // 256 287 | i -= mid * 256 288 | lsb = i 289 | return self.spi.xfer([REG.LORA.FR_MSB | 0x80, msb, mid, lsb]) 290 | 291 | def get_pa_config(self, convert_dBm=False): 292 | v = self.spi.xfer([REG.LORA.PA_CONFIG, 0])[1] 293 | pa_select = v >> 7 294 | max_power = v >> 4 & 0b111 295 | output_power = v & 0b1111 296 | if convert_dBm: 297 | max_power = max_power * .6 + 10.8 298 | output_power = max_power - (15 - output_power) 299 | return dict( 300 | pa_select = pa_select, 301 | max_power = max_power, 302 | output_power = output_power 303 | ) 304 | 305 | def set_pa_config(self, pa_select=None, max_power=None, output_power=None): 306 | """ Configure the PA 307 | :param pa_select: Selects PA output pin, 0->RFO, 1->PA_BOOST 308 | :param max_power: Select max output power Pmax=10.8+0.6*MaxPower 309 | :param output_power: Output power Pout=Pmax-(15-OutputPower) if PaSelect = 0, 310 | Pout=17-(15-OutputPower) if PaSelect = 1 (PA_BOOST pin) 311 | :return: new register value 312 | """ 313 | loc = locals() 314 | current = self.get_pa_config() 315 | loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} 316 | val = (loc['pa_select'] << 7) | (loc['max_power'] << 4) | (loc['output_power']) 317 | return self.spi.xfer([REG.LORA.PA_CONFIG | 0x80, val])[1] 318 | 319 | @getter(REG.LORA.PA_RAMP) 320 | def get_pa_ramp(self, val): 321 | return val & 0b1111 322 | 323 | @setter(REG.LORA.PA_RAMP) 324 | def set_pa_ramp(self, val): 325 | return val & 0b1111 326 | 327 | def get_ocp(self, convert_mA=False): 328 | v = self.spi.xfer([REG.LORA.OCP, 0])[1] 329 | ocp_on = v >> 5 & 0x01 330 | ocp_trim = v & 0b11111 331 | if convert_mA: 332 | if ocp_trim <= 15: 333 | ocp_trim = 45. + 5. * ocp_trim 334 | elif ocp_trim <= 27: 335 | ocp_trim = -30. + 10. * ocp_trim 336 | else: 337 | assert ocp_trim <= 27 338 | return dict( 339 | ocp_on = ocp_on, 340 | ocp_trim = ocp_trim 341 | ) 342 | 343 | def set_ocp_trim(self, I_mA): 344 | assert(I_mA >= 45 and I_mA <= 240) 345 | ocp_on = self.spi.xfer([REG.LORA.OCP, 0])[1] >> 5 & 0x01 346 | if I_mA <= 120: 347 | v = int(round((I_mA-45.)/5.)) 348 | else: 349 | v = int(round((I_mA+30.)/10.)) 350 | v = set_bit(v, 5, ocp_on) 351 | return self.spi.xfer([REG.LORA.OCP | 0x80, v])[1] 352 | 353 | def get_lna(self): 354 | v = self.spi.xfer([REG.LORA.LNA, 0])[1] 355 | return dict( 356 | lna_gain = v >> 5, 357 | lna_boost_lf = v >> 3 & 0b11, 358 | lna_boost_hf = v & 0b11 359 | ) 360 | 361 | def set_lna(self, lna_gain=None, lna_boost_lf=None, lna_boost_hf=None): 362 | assert lna_boost_hf is None or lna_boost_hf == 0b00 or lna_boost_hf == 0b11 363 | self.set_mode(MODE.STDBY) 364 | if lna_gain is not None: 365 | # Apparently agc_auto_on must be 0 in order to set lna_gain 366 | self.set_agc_auto_on(lna_gain == GAIN.NOT_USED) 367 | loc = locals() 368 | current = self.get_lna() 369 | loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} 370 | val = (loc['lna_gain'] << 5) | (loc['lna_boost_lf'] << 3) | (loc['lna_boost_hf']) 371 | retval = self.spi.xfer([REG.LORA.LNA | 0x80, val])[1] 372 | if lna_gain is not None: 373 | # agc_auto_on must track lna_gain: GAIN=NOT_USED -> agc_auto=ON, otherwise =OFF 374 | self.set_agc_auto_on(lna_gain == GAIN.NOT_USED) 375 | return retval 376 | 377 | def set_lna_gain(self, lna_gain): 378 | self.set_lna(lna_gain=lna_gain) 379 | 380 | def get_fifo_addr_ptr(self): 381 | return self.spi.xfer([REG.LORA.FIFO_ADDR_PTR, 0])[1] 382 | 383 | def set_fifo_addr_ptr(self, ptr): 384 | return self.spi.xfer([REG.LORA.FIFO_ADDR_PTR | 0x80, ptr])[1] 385 | 386 | def get_fifo_tx_base_addr(self): 387 | return self.spi.xfer([REG.LORA.FIFO_TX_BASE_ADDR, 0])[1] 388 | 389 | def set_fifo_tx_base_addr(self, ptr): 390 | return self.spi.xfer([REG.LORA.FIFO_TX_BASE_ADDR | 0x80, ptr])[1] 391 | 392 | def get_fifo_rx_base_addr(self): 393 | return self.spi.xfer([REG.LORA.FIFO_RX_BASE_ADDR, 0])[1] 394 | 395 | def set_fifo_rx_base_addr(self, ptr): 396 | return self.spi.xfer([REG.LORA.FIFO_RX_BASE_ADDR | 0x80, ptr])[1] 397 | 398 | def get_fifo_rx_current_addr(self): 399 | return self.spi.xfer([REG.LORA.FIFO_RX_CURR_ADDR, 0])[1] 400 | 401 | def get_fifo_rx_byte_addr(self): 402 | return self.spi.xfer([REG.LORA.FIFO_RX_BYTE_ADDR, 0])[1] 403 | 404 | def get_irq_flags_mask(self): 405 | v = self.spi.xfer([REG.LORA.IRQ_FLAGS_MASK, 0])[1] 406 | return dict( 407 | rx_timeout = v >> 7 & 0x01, 408 | rx_done = v >> 6 & 0x01, 409 | crc_error = v >> 5 & 0x01, 410 | valid_header = v >> 4 & 0x01, 411 | tx_done = v >> 3 & 0x01, 412 | cad_done = v >> 2 & 0x01, 413 | fhss_change_ch = v >> 1 & 0x01, 414 | cad_detected = v >> 0 & 0x01, 415 | ) 416 | 417 | def set_irq_flags_mask(self, 418 | rx_timeout=None, rx_done=None, crc_error=None, valid_header=None, tx_done=None, 419 | cad_done=None, fhss_change_ch=None, cad_detected=None): 420 | loc = locals() 421 | v = self.spi.xfer([REG.LORA.IRQ_FLAGS_MASK, 0])[1] 422 | for i, s in enumerate(['cad_detected', 'fhss_change_ch', 'cad_done', 'tx_done', 'valid_header', 423 | 'crc_error', 'rx_done', 'rx_timeout']): 424 | this_bit = locals()[s] 425 | if this_bit is not None: 426 | v = set_bit(v, i, this_bit) 427 | return self.spi.xfer([REG.LORA.IRQ_FLAGS_MASK | 0x80, v])[1] 428 | 429 | def get_irq_flags(self): 430 | v = self.spi.xfer([REG.LORA.IRQ_FLAGS, 0])[1] 431 | return dict( 432 | rx_timeout = v >> 7 & 0x01, 433 | rx_done = v >> 6 & 0x01, 434 | crc_error = v >> 5 & 0x01, 435 | valid_header = v >> 4 & 0x01, 436 | tx_done = v >> 3 & 0x01, 437 | cad_done = v >> 2 & 0x01, 438 | fhss_change_ch = v >> 1 & 0x01, 439 | cad_detected = v >> 0 & 0x01, 440 | ) 441 | 442 | def set_irq_flags(self, 443 | rx_timeout=None, rx_done=None, crc_error=None, valid_header=None, tx_done=None, 444 | cad_done=None, fhss_change_ch=None, cad_detected=None): 445 | v = self.spi.xfer([REG.LORA.IRQ_FLAGS, 0])[1] 446 | for i, s in enumerate(['cad_detected', 'fhss_change_ch', 'cad_done', 'tx_done', 'valid_header', 447 | 'crc_error', 'rx_done', 'rx_timeout']): 448 | this_bit = locals()[s] 449 | if this_bit is not None: 450 | v = set_bit(v, i, this_bit) 451 | return self.spi.xfer([REG.LORA.IRQ_FLAGS | 0x80, v])[1] 452 | 453 | def clear_irq_flags(self, 454 | RxTimeout=None, RxDone=None, PayloadCrcError=None, 455 | ValidHeader=None, TxDone=None, CadDone=None, 456 | FhssChangeChannel=None, CadDetected=None): 457 | v = 0 458 | for i, s in enumerate(['CadDetected', 'FhssChangeChannel', 'CadDone', 459 | 'TxDone', 'ValidHeader', 'PayloadCrcError', 460 | 'RxDone', 'RxTimeout']): 461 | this_bit = locals()[s] 462 | if this_bit is not None: 463 | v = set_bit(v, eval('MASK.IRQ_FLAGS.' + s), this_bit) 464 | return self.spi.xfer([REG.LORA.IRQ_FLAGS | 0x80, v])[1] 465 | 466 | 467 | def get_rx_nb_bytes(self): 468 | return self.spi.xfer([REG.LORA.RX_NB_BYTES, 0])[1] 469 | 470 | def get_rx_header_cnt(self): 471 | msb, lsb = self.spi.xfer([REG.LORA.RX_HEADER_CNT_MSB, 0, 0])[1:] 472 | return lsb + 256 * msb 473 | 474 | def get_rx_packet_cnt(self): 475 | msb, lsb = self.spi.xfer([REG.LORA.RX_PACKET_CNT_MSB, 0, 0])[1:] 476 | return lsb + 256 * msb 477 | 478 | def get_modem_status(self): 479 | status = self.spi.xfer([REG.LORA.MODEM_STAT, 0])[1] 480 | return dict( 481 | rx_coding_rate = status >> 5 & 0x03, 482 | modem_clear = status >> 4 & 0x01, 483 | header_info_valid = status >> 3 & 0x01, 484 | rx_ongoing = status >> 2 & 0x01, 485 | signal_sync = status >> 1 & 0x01, 486 | signal_detected = status >> 0 & 0x01 487 | ) 488 | 489 | def get_pkt_snr_value(self): 490 | v = self.spi.xfer([REG.LORA.PKT_SNR_VALUE, 0])[1] 491 | return float(256-v) / 4. 492 | 493 | def get_pkt_rssi_value(self): 494 | v = self.spi.xfer([REG.LORA.PKT_RSSI_VALUE, 0])[1] 495 | return v - 157 496 | 497 | def get_rssi_value(self): 498 | v = self.spi.xfer([REG.LORA.RSSI_VALUE, 0])[1] 499 | return v - 157 500 | 501 | def get_hop_channel(self): 502 | v = self.spi.xfer([REG.LORA.HOP_CHANNEL, 0])[1] 503 | return dict( 504 | pll_timeout = v >> 7, 505 | crc_on_payload = v >> 6 & 0x01, 506 | fhss_present_channel = v >> 5 & 0b111111 507 | ) 508 | 509 | def get_modem_config_1(self): 510 | val = self.spi.xfer([REG.LORA.MODEM_CONFIG_1, 0])[1] 511 | return dict( 512 | bw = val >> 4 & 0x0F, 513 | coding_rate = val >> 1 & 0x07, 514 | implicit_header_mode = val & 0x01 515 | ) 516 | 517 | def set_modem_config_1(self, bw=None, coding_rate=None, implicit_header_mode=None): 518 | loc = locals() 519 | current = self.get_modem_config_1() 520 | loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} 521 | val = loc['implicit_header_mode'] | (loc['coding_rate'] << 1) | (loc['bw'] << 4) 522 | return self.spi.xfer([REG.LORA.MODEM_CONFIG_1 | 0x80, val])[1] 523 | 524 | def set_bw(self, bw): 525 | """ Set the bandwidth 0=7.8kHz ... 9=500kHz 526 | :param bw: A number 0,2,3,...,9 527 | :return: 528 | """ 529 | self.set_modem_config_1(bw=bw) 530 | 531 | def set_coding_rate(self, coding_rate): 532 | """ Set the coding rate 4/5, 4/6, 4/7, 4/8 533 | :param coding_rate: A number 1,2,3,4 534 | :return: New register value 535 | """ 536 | self.set_modem_config_1(coding_rate=coding_rate) 537 | 538 | def set_implicit_header_mode(self, implicit_header_mode): 539 | self.set_modem_config_1(implicit_header_mode=implicit_header_mode) 540 | 541 | def get_modem_config_2(self, include_symb_timout_lsb=False): 542 | val = self.spi.xfer([REG.LORA.MODEM_CONFIG_2, 0])[1] 543 | d = dict( 544 | spreading_factor = val >> 4 & 0x0F, 545 | tx_cont_mode = val >> 3 & 0x01, 546 | rx_crc = val >> 2 & 0x01, 547 | ) 548 | if include_symb_timout_lsb: 549 | d['symb_timout_lsb'] = val & 0x03 550 | return d 551 | 552 | def set_modem_config_2(self, spreading_factor=None, tx_cont_mode=None, rx_crc=None): 553 | loc = locals() 554 | # RegModemConfig2 contains the SymbTimout MSB bits. We tack the back on when writing this register. 555 | current = self.get_modem_config_2(include_symb_timout_lsb=True) 556 | loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} 557 | val = (loc['spreading_factor'] << 4) | (loc['tx_cont_mode'] << 3) | (loc['rx_crc'] << 2) | current['symb_timout_lsb'] 558 | return self.spi.xfer([REG.LORA.MODEM_CONFIG_2 | 0x80, val])[1] 559 | 560 | def set_spreading_factor(self, spreading_factor): 561 | self.set_modem_config_2(spreading_factor=spreading_factor) 562 | 563 | def set_rx_crc(self, rx_crc): 564 | self.set_modem_config_2(rx_crc=rx_crc) 565 | 566 | def get_modem_config_3(self): 567 | val = self.spi.xfer([REG.LORA.MODEM_CONFIG_3, 0])[1] 568 | return dict( 569 | low_data_rate_optim = val >> 3 & 0x01, 570 | agc_auto_on = val >> 2 & 0x01 571 | ) 572 | 573 | def set_modem_config_3(self, low_data_rate_optim=None, agc_auto_on=None): 574 | loc = locals() 575 | current = self.get_modem_config_3() 576 | loc = {s: current[s] if loc[s] is None else loc[s] for s in loc} 577 | val = (loc['low_data_rate_optim'] << 3) | (loc['agc_auto_on'] << 2) 578 | return self.spi.xfer([REG.LORA.MODEM_CONFIG_3 | 0x80, val])[1] 579 | 580 | @setter(REG.LORA.INVERT_IQ) 581 | def set_invert_iq(self, invert): 582 | """ Invert the LoRa I and Q signals 583 | :param invert: 0: normal mode, 1: I and Q inverted 584 | :return: New value of register 585 | """ 586 | return 0x27 | (invert & 0x01) << 6 587 | 588 | @getter(REG.LORA.INVERT_IQ) 589 | def get_invert_iq(self, val): 590 | """ Get the invert the I and Q setting 591 | :return: 0: normal mode, 1: I and Q inverted 592 | """ 593 | return (val >> 6) & 0x01 594 | 595 | def get_agc_auto_on(self): 596 | return self.get_modem_config_3()['agc_auto_on'] 597 | 598 | def set_agc_auto_on(self, agc_auto_on): 599 | self.set_modem_config_3(agc_auto_on=agc_auto_on) 600 | 601 | def get_low_data_rate_optim(self): 602 | return self.set_modem_config_3()['low_data_rate_optim'] 603 | 604 | def set_low_data_rate_optim(self, low_data_rate_optim): 605 | self.set_modem_config_3(low_data_rate_optim=low_data_rate_optim) 606 | 607 | def get_symb_timeout(self): 608 | SYMB_TIMEOUT_MSB = REG.LORA.MODEM_CONFIG_2 609 | msb, lsb = self.spi.xfer([SYMB_TIMEOUT_MSB, 0, 0])[1:] # the MSB bits are stored in REG.LORA.MODEM_CONFIG_2 610 | msb = msb & 0b11 611 | return lsb + 256 * msb 612 | 613 | def set_symb_timeout(self, timeout): 614 | bkup_reg_modem_config_2 = self.spi.xfer([REG.LORA.MODEM_CONFIG_2, 0])[1] 615 | msb = timeout >> 8 & 0b11 # bits 8-9 616 | lsb = timeout - 256 * msb # bits 0-7 617 | reg_modem_config_2 = bkup_reg_modem_config_2 & 0xFC | msb # bits 2-7 of bkup_reg_modem_config_2 ORed with the two msb bits 618 | old_msb = self.spi.xfer([REG.LORA.MODEM_CONFIG_2 | 0x80, reg_modem_config_2])[1] & 0x03 619 | old_lsb = self.spi.xfer([REG.LORA.SYMB_TIMEOUT_LSB | 0x80, lsb])[1] 620 | return old_lsb + 256 * old_msb 621 | 622 | def get_preamble(self): 623 | msb, lsb = self.spi.xfer([REG.LORA.PREAMBLE_MSB, 0, 0])[1:] 624 | return lsb + 256 * msb 625 | 626 | def set_preamble(self, preamble): 627 | msb = preamble >> 8 628 | lsb = preamble - msb * 256 629 | old_msb, old_lsb = self.spi.xfer([REG.LORA.PREAMBLE_MSB | 0x80, msb, lsb])[1:] 630 | return old_lsb + 256 * old_msb 631 | 632 | @getter(REG.LORA.PAYLOAD_LENGTH) 633 | def get_payload_length(self, val): 634 | return val 635 | 636 | @setter(REG.LORA.PAYLOAD_LENGTH) 637 | def set_payload_length(self, payload_length): 638 | return payload_length 639 | 640 | @getter(REG.LORA.MAX_PAYLOAD_LENGTH) 641 | def get_max_payload_length(self, val): 642 | return val 643 | 644 | @setter(REG.LORA.MAX_PAYLOAD_LENGTH) 645 | def set_max_payload_length(self, max_payload_length): 646 | return max_payload_length 647 | 648 | @getter(REG.LORA.HOP_PERIOD) 649 | def get_hop_period(self, val): 650 | return val 651 | 652 | @setter(REG.LORA.HOP_PERIOD) 653 | def set_hop_period(self, hop_period): 654 | return hop_period 655 | 656 | def get_fei(self): 657 | msb, mid, lsb = self.spi.xfer([REG.LORA.FEI_MSB, 0, 0, 0])[1:] 658 | msb &= 0x0F 659 | freq_error = lsb + 256 * (mid + 256 * msb) 660 | return freq_error 661 | 662 | @getter(REG.LORA.DETECT_OPTIMIZE) 663 | def get_detect_optimize(self, val): 664 | """ Get LoRa detection optimize setting 665 | :return: detection optimize setting 0x03: SF7-12, 0x05: SF6 666 | 667 | """ 668 | return val & 0b111 669 | 670 | @setter(REG.LORA.DETECT_OPTIMIZE) 671 | def set_detect_optimize(self, detect_optimize): 672 | """ Set LoRa detection optimize 673 | :param detect_optimize 0x03: SF7-12, 0x05: SF6 674 | :return: New register value 675 | """ 676 | assert detect_optimize == 0x03 or detect_optimize == 0x05 677 | return detect_optimize & 0b111 678 | 679 | @getter(REG.LORA.DETECTION_THRESH) 680 | def get_detection_threshold(self, val): 681 | """ Get LoRa detection threshold setting 682 | :return: detection threshold 0x0A: SF7-12, 0x0C: SF6 683 | 684 | """ 685 | return val 686 | 687 | @setter(REG.LORA.DETECTION_THRESH) 688 | def set_detection_threshold(self, detect_threshold): 689 | """ Set LoRa detection optimize 690 | :param detect_threshold 0x0A: SF7-12, 0x0C: SF6 691 | :return: New register value 692 | """ 693 | assert detect_threshold == 0x0A or detect_threshold == 0x0C 694 | return detect_threshold 695 | 696 | @getter(REG.LORA.SYNC_WORD) 697 | def get_sync_word(self, sync_word): 698 | return sync_word 699 | 700 | @setter(REG.LORA.SYNC_WORD) 701 | def set_sync_word(self, sync_word): 702 | return sync_word 703 | 704 | @getter(REG.LORA.DIO_MAPPING_1) 705 | def get_dio_mapping_1(self, mapping): 706 | """ Get mapping of pins DIO0 to DIO3. Object variable dio_mapping will be set. 707 | :param mapping: Register value 708 | :type mapping: int 709 | :return: Value of the mapping list 710 | :rtype: list[int] 711 | """ 712 | self.dio_mapping = [mapping>>6 & 0x03, mapping>>4 & 0x03, mapping>>2 & 0x03, mapping>>0 & 0x03] \ 713 | + self.dio_mapping[4:6] 714 | return self.dio_mapping 715 | 716 | @setter(REG.LORA.DIO_MAPPING_1) 717 | def set_dio_mapping_1(self, mapping): 718 | """ Set mapping of pins DIO0 to DIO3. Object variable dio_mapping will be set. 719 | :param mapping: Register value 720 | :type mapping: int 721 | :return: New value of the register 722 | :rtype: int 723 | """ 724 | self.dio_mapping = [mapping>>6 & 0x03, mapping>>4 & 0x03, mapping>>2 & 0x03, mapping>>0 & 0x03] \ 725 | + self.dio_mapping[4:6] 726 | return mapping 727 | 728 | @getter(REG.LORA.DIO_MAPPING_2) 729 | def get_dio_mapping_2(self, mapping): 730 | """ Get mapping of pins DIO4 to DIO5. Object variable dio_mapping will be set. 731 | :param mapping: Register value 732 | :type mapping: int 733 | :return: Value of the mapping list 734 | :rtype: list[int] 735 | """ 736 | self.dio_mapping = self.dio_mapping[0:4] + [mapping>>6 & 0x03, mapping>>4 & 0x03] 737 | return self.dio_mapping 738 | 739 | @setter(REG.LORA.DIO_MAPPING_2) 740 | def set_dio_mapping_2(self, mapping): 741 | """ Set mapping of pins DIO4 to DIO5. Object variable dio_mapping will be set. 742 | :param mapping: Register value 743 | :type mapping: int 744 | :return: New value of the register 745 | :rtype: int 746 | """ 747 | assert mapping & 0b00001110 == 0 748 | self.dio_mapping = self.dio_mapping[0:4] + [mapping>>6 & 0x03, mapping>>4 & 0x03] 749 | return mapping 750 | 751 | def get_dio_mapping(self): 752 | """ Utility function that returns the list of current DIO mappings. Object variable dio_mapping will be set. 753 | :return: List of current DIO mappings 754 | :rtype: list[int] 755 | """ 756 | self.get_dio_mapping_1() 757 | return self.get_dio_mapping_2() 758 | 759 | def set_dio_mapping(self, mapping): 760 | """ Utility function that returns the list of current DIO mappings. Object variable dio_mapping will be set. 761 | :param mapping: DIO mapping list 762 | :type mapping: list[int] 763 | :return: New DIO mapping list 764 | :rtype: list[int] 765 | """ 766 | mapping_1 = (mapping[0] & 0x03) << 6 | (mapping[1] & 0x03) << 4 | (mapping[2] & 0x3) << 2 | mapping[3] & 0x3 767 | mapping_2 = (mapping[4] & 0x03) << 6 | (mapping[5] & 0x03) << 4 768 | self.set_dio_mapping_1(mapping_1) 769 | return self.set_dio_mapping_2(mapping_2) 770 | 771 | @getter(REG.LORA.VERSION) 772 | def get_version(self, version): 773 | """ Version code of the chip. 774 | Bits 7-4 give the full revision number; bits 3-0 give the metal mask revision number. 775 | :return: Version code 776 | :rtype: int 777 | """ 778 | return version 779 | 780 | @getter(REG.LORA.TCXO) 781 | def get_tcxo(self, tcxo): 782 | """ Get TCXO or XTAL input setting 783 | 0 -> "XTAL": Crystal Oscillator with external Crystal 784 | 1 -> "TCXO": External clipped sine TCXO AC-connected to XTA pin 785 | :param tcxo: 1=TCXO or 0=XTAL input setting 786 | :return: TCXO or XTAL input setting 787 | :type: int (0 or 1) 788 | """ 789 | return tcxo & 0b00010000 790 | 791 | @setter(REG.LORA.TCXO) 792 | def set_tcxo(self, tcxo): 793 | """ Make TCXO or XTAL input setting. 794 | 0 -> "XTAL": Crystal Oscillator with external Crystal 795 | 1 -> "TCXO": External clipped sine TCXO AC-connected to XTA pin 796 | :param tcxo: 1=TCXO or 0=XTAL input setting 797 | :return: new TCXO or XTAL input setting 798 | """ 799 | return (tcxo >= 1) << 4 | 0x09 # bits 0-3 must be 0b1001 800 | 801 | @getter(REG.LORA.PA_DAC) 802 | def get_pa_dac(self, pa_dac): 803 | """ Enables the +20dBm option on PA_BOOST pin 804 | False -> Default value 805 | True -> +20dBm on PA_BOOST when OutputPower=1111 806 | :return: True/False if +20dBm option on PA_BOOST on/off 807 | :rtype: bool 808 | """ 809 | pa_dac &= 0x07 # only bits 0-2 810 | if pa_dac == 0x04: 811 | return False 812 | elif pa_dac == 0x07: 813 | return True 814 | else: 815 | raise RuntimeError("Bad PA_DAC value %s" % hex(pa_dac)) 816 | 817 | @setter(REG.LORA.PA_DAC) 818 | def set_pa_dac(self, pa_dac): 819 | """ Enables the +20dBm option on PA_BOOST pin 820 | False -> Default value 821 | True -> +20dBm on PA_BOOST when OutputPower=1111 822 | :param pa_dac: 1/0 if +20dBm option on PA_BOOST on/off 823 | :return: New pa_dac register value 824 | :rtype: int 825 | """ 826 | return 0x87 if pa_dac else 0x84 827 | 828 | def rx_chain_calibration(self, freq=868.): 829 | """ Run the image calibration (see Semtech documentation section 4.2.3.8) 830 | :param freq: Frequency for the HF calibration 831 | :return: None 832 | """ 833 | # backup some registers 834 | op_mode_bkup = self.get_mode() 835 | pa_config_bkup = self.get_register(REG.LORA.PA_CONFIG) 836 | freq_bkup = self.get_freq() 837 | # for image calibration device must be in FSK standby mode 838 | self.set_mode(MODE.FSK_STDBY) 839 | # cut the PA 840 | self.set_register(REG.LORA.PA_CONFIG, 0x00) 841 | # calibration for the LF band 842 | image_cal = (self.get_register(REG.FSK.IMAGE_CAL) & 0xBF) | 0x40 843 | self.set_register(REG.FSK.IMAGE_CAL, image_cal) 844 | while (self.get_register(REG.FSK.IMAGE_CAL) & 0x20) == 0x20: 845 | pass 846 | # Set a Frequency in HF band 847 | self.set_freq(freq) 848 | # calibration for the HF band 849 | image_cal = (self.get_register(REG.FSK.IMAGE_CAL) & 0xBF) | 0x40 850 | self.set_register(REG.FSK.IMAGE_CAL, image_cal) 851 | while (self.get_register(REG.FSK.IMAGE_CAL) & 0x20) == 0x20: 852 | pass 853 | # put back the saved parameters 854 | self.set_mode(op_mode_bkup) 855 | self.set_register(REG.LORA.PA_CONFIG, pa_config_bkup) 856 | self.set_freq(freq_bkup) 857 | 858 | def dump_registers(self): 859 | """ Returns a list of [reg_addr, reg_name, reg_value] tuples. Chip is put into mode SLEEP. 860 | :return: List of [reg_addr, reg_name, reg_value] tuples 861 | :rtype: list[tuple] 862 | """ 863 | self.set_mode(MODE.SLEEP) 864 | values = self.get_all_registers() 865 | skip_set = set([REG.LORA.FIFO]) 866 | result_list = [] 867 | for i, s in REG.LORA.lookup.iteritems(): 868 | if i in skip_set: 869 | continue 870 | v = values[i] 871 | result_list.append((i, s, v)) 872 | return result_list 873 | 874 | def get_register(self, register_address): 875 | return self.spi.xfer([register_address & 0x7F, 0])[1] 876 | 877 | def set_register(self, register_address, val): 878 | return self.spi.xfer([register_address | 0x80, val])[1] 879 | 880 | def get_all_registers(self): 881 | # read all registers 882 | reg = [0] + self.spi.xfer([1]+[0]*0x3E)[1:] 883 | self.mode = reg[1] 884 | return reg 885 | 886 | def __del__(self): 887 | self.set_mode(MODE.SLEEP) 888 | if self.verbose: 889 | sys.stderr.write("MODE=SLEEP\n") 890 | 891 | def __str__(self): 892 | # don't use __str__ while in any mode other that SLEEP or STDBY 893 | assert(self.mode == MODE.SLEEP or self.mode == MODE.STDBY) 894 | 895 | onoff = lambda i: 'ON' if i else 'OFF' 896 | f = self.get_freq() 897 | cfg1 = self.get_modem_config_1() 898 | cfg2 = self.get_modem_config_2() 899 | cfg3 = self.get_modem_config_3() 900 | pa_config = self.get_pa_config(convert_dBm=True) 901 | ocp = self.get_ocp(convert_mA=True) 902 | lna = self.get_lna() 903 | s = "SX127x LoRa registers:\n" 904 | s += " mode %s\n" % MODE.lookup[self.get_mode()] 905 | s += " freq %f MHz\n" % f 906 | s += " coding_rate %s\n" % CODING_RATE.lookup[cfg1['coding_rate']] 907 | s += " bw %s\n" % BW.lookup[cfg1['bw']] 908 | s += " spreading_factor %s chips/symb\n" % (1 << cfg2['spreading_factor']) 909 | s += " implicit_hdr_mode %s\n" % onoff(cfg1['implicit_header_mode']) 910 | s += " rx_payload_crc %s\n" % onoff(cfg2['rx_crc']) 911 | s += " tx_cont_mode %s\n" % onoff(cfg2['tx_cont_mode']) 912 | s += " preamble %d\n" % self.get_preamble() 913 | s += " low_data_rate_opti %s\n" % onoff(cfg3['low_data_rate_optim']) 914 | s += " agc_auto_on %s\n" % onoff(cfg3['agc_auto_on']) 915 | s += " symb_timeout %s\n" % self.get_symb_timeout() 916 | s += " freq_hop_period %s\n" % self.get_hop_period() 917 | s += " hop_channel %s\n" % self.get_hop_channel() 918 | s += " payload_length %s\n" % self.get_payload_length() 919 | s += " max_payload_length %s\n" % self.get_max_payload_length() 920 | s += " irq_flags_mask %s\n" % self.get_irq_flags_mask() 921 | s += " irq_flags %s\n" % self.get_irq_flags() 922 | s += " rx_nb_byte %d\n" % self.get_rx_nb_bytes() 923 | s += " rx_header_cnt %d\n" % self.get_rx_header_cnt() 924 | s += " rx_packet_cnt %d\n" % self.get_rx_packet_cnt() 925 | s += " pkt_snr_value %f\n" % self.get_pkt_snr_value() 926 | s += " pkt_rssi_value %d\n" % self.get_pkt_rssi_value() 927 | s += " rssi_value %d\n" % self.get_rssi_value() 928 | s += " fei %d\n" % self.get_fei() 929 | s += " pa_select %s\n" % PA_SELECT.lookup[pa_config['pa_select']] 930 | s += " max_power %f dBm\n" % pa_config['max_power'] 931 | s += " output_power %f dBm\n" % pa_config['output_power'] 932 | s += " ocp %s\n" % onoff(ocp['ocp_on']) 933 | s += " ocp_trim %f mA\n" % ocp['ocp_trim'] 934 | s += " lna_gain %s\n" % GAIN.lookup[lna['lna_gain']] 935 | s += " lna_boost_lf %s\n" % bin(lna['lna_boost_lf']) 936 | s += " lna_boost_hf %s\n" % bin(lna['lna_boost_hf']) 937 | s += " detect_optimize %#02x\n" % self.get_detect_optimize() 938 | s += " detection_thresh %#02x\n" % self.get_detection_threshold() 939 | s += " sync_word %#02x\n" % self.get_sync_word() 940 | s += " dio_mapping 0..5 %s\n" % self.get_dio_mapping() 941 | s += " tcxo %s\n" % ['XTAL', 'TCXO'][self.get_tcxo()] 942 | s += " pa_dac %s\n" % ['default', 'PA_BOOST'][self.get_pa_dac()] 943 | s += " fifo_addr_ptr %#02x\n" % self.get_fifo_addr_ptr() 944 | s += " fifo_tx_base_addr %#02x\n" % self.get_fifo_tx_base_addr() 945 | s += " fifo_rx_base_addr %#02x\n" % self.get_fifo_rx_base_addr() 946 | s += " fifo_rx_curr_addr %#02x\n" % self.get_fifo_rx_current_addr() 947 | s += " fifo_rx_byte_addr %#02x\n" % self.get_fifo_rx_byte_addr() 948 | s += " status %s\n" % self.get_modem_status() 949 | s += " version %#02x\n" % self.get_version() 950 | return s 951 | --------------------------------------------------------------------------------