├── .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 |
--------------------------------------------------------------------------------