diff --git a/.gitignore b/.gitignore index f5000dc..ed11a67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc -keys.py +.env +frame.txt diff --git a/LoRaWAN/DataPayload.py b/LoRaWAN/DataPayload.py index 6a35513..5ff8d41 100644 --- a/LoRaWAN/DataPayload.py +++ b/LoRaWAN/DataPayload.py @@ -56,7 +56,7 @@ def decrypt_payload(self, key, direction, mic): a += [0x00] a += [i+1] - cipher = AES.new(bytes(key)) + cipher = AES.new(bytes(key), AES.MODE_ECB) s = cipher.encrypt(bytes(a)) padded_payload = [] @@ -84,7 +84,7 @@ def encrypt_payload(self, key, direction, data): a += [0x00] a += [i+1] - cipher = AES.new(bytes(key)) + cipher = AES.new(bytes(key), AES.MODE_ECB) s = cipher.encrypt(bytes(a)) padded_payload = [] diff --git a/LoRaWAN/JoinAcceptPayload.py b/LoRaWAN/JoinAcceptPayload.py index 36afa39..7d4be95 100644 --- a/LoRaWAN/JoinAcceptPayload.py +++ b/LoRaWAN/JoinAcceptPayload.py @@ -56,7 +56,7 @@ def decrypt_payload(self, key, direction, mic): a += self.encrypted_payload a += mic - cipher = AES.new(bytes(key)) + cipher = AES.new(bytes(key), AES.MODE_ECB) self.payload = cipher.encrypt(bytes(a))[:-4] self.appnonce = self.payload[:3] @@ -75,7 +75,7 @@ def encrypt_payload(self, key, direction, mhdr): a += self.to_clear_raw() a += self.compute_mic(key, direction, mhdr) - cipher = AES.new(bytes(key)) + cipher = AES.new(bytes(key), AES.MODE_ECB) return list(map(int, cipher.decrypt(bytes(a)))) def derive_nwskey(self, key, devnonce): @@ -85,7 +85,7 @@ def derive_nwskey(self, key, devnonce): a += devnonce a += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - cipher = AES.new(bytes(key)) + cipher = AES.new(bytes(key), AES.MODE_ECB) return list(map(int, cipher.encrypt(bytes(a)))) def derive_appskey(self, key, devnonce): @@ -95,5 +95,5 @@ def derive_appskey(self, key, devnonce): a += devnonce a += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - cipher = AES.new(bytes(key)) + cipher = AES.new(bytes(key), AES.MODE_ECB) return list(map(int, cipher.encrypt(bytes(a)))) diff --git a/README.md b/README.md index 03075e1..7801724 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +#RPI install routine +``` +sudo apt-get install -y tmux htop +sudo pip3 install adafruit-circuitpython-ssd1306 +sudo pip3 install adafruit-circuitpython-framebuf +sudo pip3 install adafruit-circuitpython-rfm9x +sudo pip3 install adafruit-circuitpython-gps +sudo pip3 install pycryptodome +sudo pip3 install shortuuid +sudo pip3 install python-dotenv +sudo apt-get install gpsd gpsd-clients +git clone git@github.com:DGaffney/LoRaWAN.git +``` + # LoRaWAN This is a LoRaWAN v1.0 implementation in python. @@ -9,13 +23,39 @@ This fork adds support for the Adafruit LoRA Radio Bonnet with OLED - RFM95W @ 9 It also allows you to connect as a client to the Helium Network. -You must create a device on the Helium Console at https://console.helium.com/ - -You need to rename "keys_example.py" to "keys.py" and enter you device information from the Helium Console. ## Installation -To register a device and get a device ID you need to run otaa_helium.py and stores the results in keys.py. + +After forking the repo, install the python libs: + + pip3 install -r requirements.txt + +## Credentials + +The keys can be added to a `./.env` file: + +``` +deveui = 1234ASD432 +appeui = 234ASD4321 +appkey = 34ASD43210 + +devaddr = [00,11,22] +nwskey = [11,22,33] +appskey = [22,33,44] +``` + +They can also be passed as environment variables: + +``` +deveui=1234ASD432 python3 rssi_helium.py +``` + +## Registration + +You must create a device on the Helium Console at https://console.helium.com/ + +After copying the device credentials (the first three in the `.env` file, above), run otaa_helium.py, which will get the final three credentials for the `.env` file. Then you can run tx_helium.py to send messages by specifying the msg and the frame like this: diff --git a/frame.py b/frame.py deleted file mode 100644 index 61c3e5f..0000000 --- a/frame.py +++ /dev/null @@ -1 +0,0 @@ -frame = 1 \ No newline at end of file diff --git a/gps.py b/gps.py new file mode 100644 index 0000000..60c9c4f --- /dev/null +++ b/gps.py @@ -0,0 +1,50 @@ +from math import sin, cos, sqrt, atan2, radians +import datetime +from gpsdclient import GPSDClient + +client = GPSDClient(host="127.0.0.1") +def get_gps_data(transact_timeout=10): + start = datetime.datetime.now() + for result in client.dict_stream(convert_datetime=True): + (datetime.datetime.now() - start).seconds < transact_timeout + if result["class"] == "TPV": + return {"lat": result.get("lat"), "lon": result.get("lon"), "speed": result.get("speed"), "alt": result.get("alt")} + + +def get_dist(lat1,lon1,lat2,lon2): + # approximate radius of earth in km + R = 6373.0 + lat1 = radians(lat1) + lon1 = radians(lon1) + lat2 = radians(lat2) + lon2 = radians(lon2) + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 + print(a) + if not a: + return 0 + c = 2 * atan2(sqrt(a), sqrt(1 - a)) + distance = R * c + return distance + +def should_send_gps(new, old, last_sent_at): + if last_sent_at is None: + return True + if not new: + return False + if new and not old: + return True + if new.get("lat") is not None and new.get("lon") is not None and None in [old.get("lat"), old.get("lon")]: + return True + if None not in [new["lat"], new["lon"], old["lat"], old["lon"]]: + distance = get_dist(new["lat"], new["lon"], old["lat"], old["lon"]) + if distance < 0.05: + if (datetime.datetime.now() - last_sent_at).seconds > 60*5: + return True + else: + return False + else: + return True + else: + return False diff --git a/gps_simpletest.py b/gps_simpletest.py new file mode 100644 index 0000000..b5bc841 --- /dev/null +++ b/gps_simpletest.py @@ -0,0 +1,109 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Simple GPS module demonstration. +# Will wait for a fix and print a message every second with the current location +# and other details. +import time +import board +import busio + +import adafruit_gps + +# Create a serial connection for the GPS connection using default speed and +# a slightly higher timeout (GPS modules typically update once a second). +# These are the defaults you should use for the GPS FeatherWing. +# For other boards set RX = GPS module TX, and TX = GPS module RX pins. +# uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) + +# for a computer, use the pyserial library for uart access +import serial +uart = serial.Serial("/dev/ttyUSB1", baudrate=9600, timeout=10) + +# If using I2C, we'll create an I2C interface to talk to using default pins +# i2c = board.I2C() + +# Create a GPS module instance. +gps = adafruit_gps.GPS(uart, debug=False) # Use UART/pyserial +# gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface + +# Initialize the GPS module by changing what data it sends and at what rate. +# These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and +# PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust +# the GPS module behavior: +# https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf + +# Turn on the basic GGA and RMC info (what you typically want) +gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") +# Turn on just minimum info (RMC only, location): +# gps.send_command(b'PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') +# Turn off everything: +# gps.send_command(b'PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') +# Turn on everything (not all of it is parsed!) +# gps.send_command(b'PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0') + +# Set update rate to once a second (1hz) which is what you typically want. +gps.send_command(b"PMTK220,1000") +# Or decrease to once every two seconds by doubling the millisecond value. +# Be sure to also increase your UART timeout above! +# gps.send_command(b'PMTK220,2000') +# You can also speed up the rate, but don't go too fast or else you can lose +# data during parsing. This would be twice a second (2hz, 500ms delay): +# gps.send_command(b'PMTK220,500') + +# Main loop runs forever printing the location, etc. every second. +last_print = time.monotonic() +while True: + # Make sure to call gps.update() every loop iteration and at least twice + # as fast as data comes from the GPS unit (usually every second). + # This returns a bool that's true if it parsed new data (you can ignore it + # though if you don't care and instead look at the has_fix property). + gps.update() + # Every second print out current location details if there's a fix. + current = time.monotonic() + if current - last_print >= 1.0: + last_print = current + if not gps.has_fix: + # Try again if we don't have a fix yet. + print("Waiting for fix...") + continue + # We have a fix! (gps.has_fix is true) + # Print out details about the fix like location, date, etc. + print("=" * 40) # Print a separator line. + print( + "Fix timestamp: {}/{}/{} {:02}:{:02}:{:02}".format( + gps.timestamp_utc.tm_mon, # Grab parts of the time from the + gps.timestamp_utc.tm_mday, # struct_time object that holds + gps.timestamp_utc.tm_year, # the fix time. Note you might + gps.timestamp_utc.tm_hour, # not get all data like year, day, + gps.timestamp_utc.tm_min, # month! + gps.timestamp_utc.tm_sec, + ) + ) + print("Latitude: {0:.6f} degrees".format(gps.latitude)) + print("Longitude: {0:.6f} degrees".format(gps.longitude)) + print( + "Precise Latitude: {:2.}{:2.4f} degrees".format( + gps.latitude_degrees, gps.latitude_minutes + ) + ) + print( + "Precise Longitude: {:2.}{:2.4f} degrees".format( + gps.longitude_degrees, gps.longitude_minutes + ) + ) + print("Fix quality: {}".format(gps.fix_quality)) + # Some attributes beyond latitude, longitude and timestamp are optional + # and might not be present. Check if they're None before trying to use! + if gps.satellites is not None: + print("# satellites: {}".format(gps.satellites)) + if gps.altitude_m is not None: + print("Altitude: {} meters".format(gps.altitude_m)) + if gps.speed_knots is not None: + print("Speed: {} knots".format(gps.speed_knots)) + if gps.track_angle_deg is not None: + print("Track angle: {} degrees".format(gps.track_angle_deg)) + if gps.horizontal_dilution is not None: + print("Horizontal dilution: {}".format(gps.horizontal_dilution)) + if gps.height_geoid is not None: + print("Height geoid: {} meters".format(gps.height_geoid)) \ No newline at end of file diff --git a/helium.py b/helium.py index bb349a8..26fb6c9 100644 --- a/helium.py +++ b/helium.py @@ -1,2 +1,42 @@ -UPFREQ = 903.9 -DOWNFREQ = 923.3 \ No newline at end of file +import time +import requests +from helium_authenticator import HeliumAuthenticator +from helium_transactor import keys, HeliumTransactor + +class Helium: + def authenticate(self): + try_count = 0 + while try_count < 5: + try: + authentication = HeliumAuthenticator.authenticate() + cur_keys = self.keys + for k,v in authentication.items(): + cur_keys[k] = v + keys.write(cur_keys) + except: + print("Failed Auth") + try_count += 1 + time.sleep(3) + return authentication + + def register_device(self): + # if we have never seen the device, build some ... machinery and APIs etc to register this device. + # response = requests.get("http://somewebsite.com/register_device.json"+some_authentication_information) + return None + + def get_last_message(self): + return self.ht.last_message + + def transact(self, msg): + return self.ht.transact(msg) + + def __init__(self, passed_keys=None, verbose=False): + self.keys = keys.get_keys() + if not self.keys.get("deveui"): + self.register_device() + if not self.keys.get("nwskey"): + self.authenticate() + helium_keys = passed_keys or keys.get_keys() + self.ht = HeliumTransactor.init(verbose, helium_keys) + + \ No newline at end of file diff --git a/helium_authenticator.py b/helium_authenticator.py new file mode 100755 index 0000000..d5ad81a --- /dev/null +++ b/helium_authenticator.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep +from SX127x.LoRa import * +from SX127x.LoRaArgumentParser import LoRaArgumentParser +from SX127x.board_config_ada import BOARD +import LoRaWAN +from LoRaWAN.MHDR import MHDR +from random import randrange +import reset_ada +import helium_helper +import keys + +class HeliumAuthenticator(LoRa): + def __init__(self, verbose=False, keys=keys.get_keys()): + super(HeliumAuthenticator, self).__init__(verbose) + self.authenticated = False + self.devaddr = None + self.nwskey = None + self.appskey = None + self.devnonce = [randrange(256), randrange(256)] + self.keys = keys + + def on_rx_done(self): + print("RxDone") + + self.clear_irq_flags(RxDone=1) + payload = self.read_payload(nocheck=True) + print(payload) + self.set_mode(MODE.SLEEP) + self.get_all_registers() + print(self) + lorawan = LoRaWAN.new([], self.keys["appkey"]) + lorawan.read(payload) + print(lorawan.get_payload()) + print(lorawan.get_mhdr().get_mversion()) + + if lorawan.get_mhdr().get_mtype() == MHDR.JOIN_ACCEPT: + self.devaddr = lorawan.get_devaddr() + self.nwskey = lorawan.derive_nwskey(self.devnonce) + self.appskey = lorawan.derive_appskey(self.devnonce) + self.authenticated = True + print("Got LoRaWAN message continue listen for join accept") + + def on_tx_done(self): + self.clear_irq_flags(TxDone=1) + self.set_mode(MODE.SLEEP) + self.set_dio_mapping([0,0,0,0,0,0]) + self.set_invert_iq(1) + self.reset_ptr_rx() + self.set_freq(helium_helper.DOWNFREQ)#915) + self.set_spreading_factor(7)#12) + self.set_bw(9) #500Khz + self.set_rx_crc(False)#TRUE + self.set_mode(MODE.RXCONT) + + def setup_tx(self): + self.set_mode(MODE.SLEEP) + self.set_dio_mapping([1,0,0,0,0,0]) + self.set_freq(helium_helper.UPFREQ) + self.set_pa_config(pa_select=1) + self.set_spreading_factor(7) + self.set_pa_config(max_power=0x0F, output_power=0x0E) + self.set_sync_word(0x34) + self.set_rx_crc(True) + self.get_all_registers() + assert(self.get_agc_auto_on() == 1) + + def start(self): + self.setup_tx() + lorawan = LoRaWAN.new(self.keys["appkey"]) + lorawan.create(MHDR.JOIN_REQUEST, {'deveui': self.keys["deveui"], 'appeui': self.keys["appeui"], 'devnonce': self.devnonce}) + self.write_payload(lorawan.to_raw()) + self.set_mode(MODE.TX) + sleep(10) + + @classmethod + def authenticate(cls): + BOARD.setup() + lora = cls(True) + try: + print("Sending LoRaWAN join request\n") + lora.start() + lora.set_mode(MODE.SLEEP) + print(lora) + except KeyboardInterrupt: + sys.stdout.flush() + print("\nKeyboardInterrupt") + finally: + sys.stdout.flush() + lora.set_mode(MODE.SLEEP) + BOARD.teardown() + return {"devaddr": lora.devaddr, "nwskey": lora.nwskey, "appskey": lora.appskey} + \ No newline at end of file diff --git a/helium_helper.py b/helium_helper.py new file mode 100644 index 0000000..bb349a8 --- /dev/null +++ b/helium_helper.py @@ -0,0 +1,2 @@ +UPFREQ = 903.9 +DOWNFREQ = 923.3 \ No newline at end of file diff --git a/helium_transactor.py b/helium_transactor.py new file mode 100755 index 0000000..cbf22ed --- /dev/null +++ b/helium_transactor.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 + +import os +import re +import json +import datetime +from time import sleep +from random import randrange + +from SX127x.LoRa import LoRa, MODE +from SX127x.board_config_ada import BOARD +import LoRaWAN +from LoRaWAN.MHDR import MHDR +import shortuuid + +import helium_helper +import keys +class HeliumTransactor(LoRa): + def __init__(self, verbose=False, keys=keys.get_keys()): + super(HeliumTransactor, self).__init__(verbose) + self.iter = 0 + self.uuid = shortuuid.uuid() + self.ack = True + self.last_tx = datetime.datetime.fromtimestamp(0) + self.last_message = None + self.transact_timeout = 5 + self.keys = keys + + def on_rx_done(self): + self.clear_irq_flags(RxDone=1) + payload = self.read_payload(nocheck=True) + lorawan = LoRaWAN.new(self.keys["nwskey"], self.keys["appskey"]) + lorawan.read(payload) + decoded = "".join(list(map(chr, lorawan.get_payload()))) + try: + self.last_message = json.loads(decoded) + except: + self.last_message = decoded + if lorawan.get_mhdr().get_mtype() == MHDR.UNCONF_DATA_DOWN: + downlink = decoded + res = lorawan.mac_payload.get_fhdr().get_fctrl() + if 0x20 & res != 0: # Check Ack bit. + if len(downlink) == 0: + downlink = "Server ack" + elif lorawan.get_mhdr().get_mtype() == MHDR.CONF_DATA_DOWN: + self.ack = True + downlink = decoded + elif lorawan.get_mhdr().get_mtype() == MHDR.CONF_DATA_UP: + downlink = decoded + else: + downlink = '' + self.set_mode(MODE.STDBY) + + def increment(self): + self.tx_counter += 1 + data_file = open("frame.txt", "w") + data_file.write(f'frame = {self.tx_counter:d}\n') + data_file.close() + + def tx(self, msg, conf=False): + if conf: + data = MHDR.CONF_DATA_UP + else: + data = MHDR.UNCONF_DATA_UP + self.increment() + lorawan = LoRaWAN.new(self.keys["nwskey"], self.keys["appskey"]) + base = {'devaddr': self.keys["devaddr"], 'fcnt': self.tx_counter, 'data': list(map(ord, msg))} + if self.ack: + lorawan.create(data, dict(**base, **{'ack':True})) + self.ack = False + else: + lorawan.create(data, base) + self.write_payload(lorawan.to_raw()) + self.set_mode(MODE.TX) + + def set_frame(self,frame): + self.tx_counter = frame + + def setup_tx(self): + # Setup + self.clear_irq_flags(RxDone=1) + self.set_mode(MODE.SLEEP) + self.set_dio_mapping([1,0,0,0,0,0]) + self.set_freq(helium_helper.UPFREQ) + self.set_bw(7) + self.set_spreading_factor(7) + self.set_pa_config(max_power=0x0F, output_power=0x0E) + self.set_sync_word(0x34) + self.set_rx_crc(True) + self.set_invert_iq(0) + assert(self.get_agc_auto_on() == 1) + + def on_tx_done(self): + self.clear_irq_flags(TxDone=1) + self.set_mode(MODE.SLEEP) + self.set_dio_mapping([0,0,0,0,0,0]) + self.set_freq(helium_helper.DOWNFREQ) + self.set_bw(9) + self.set_spreading_factor(7) + self.set_pa_config(pa_select=1) + self.set_sync_word(0x34) + self.set_rx_crc(False) + self.set_invert_iq(1) + self.reset_ptr_rx() + self.set_mode(MODE.RXCONT) + + def transact(self, msg): + self.setup_tx() + self.tx(msg, True) + # Full package - too heavy: + # self.tx(json.dumps({"i": self.iter, "s": self.uuid, "m": msg}), True) + self.iter = self.iter+1 + self.last_tx = datetime.datetime.now() + while (datetime.datetime.now() - self.last_tx).seconds < self.transact_timeout: + if self.last_message is None: + sleep(0.1) + else: + message = self.last_message + self.last_message = None + return message + + def stop(self): + self.set_mode(MODE.SLEEP) + BOARD.teardown() + + def init_frame(self): + frame = 0 + if os.path.exists('frame.txt'): + with open('frame.txt') as df: + for line in df: + if m := re.match('^frame\s*=\s*(\d+)', line): + frame = int(m.group(1)) + self.set_frame(frame) + + @classmethod + def init(cls, verbose=False, keys=keys.get_keys()): + BOARD.setup() + lora = cls(verbose, keys) + lora.init_frame() + return lora diff --git a/keys.py b/keys.py new file mode 100644 index 0000000..fbae677 --- /dev/null +++ b/keys.py @@ -0,0 +1,50 @@ +import dotenv +import json +import os + +# puts the keys from .env file into the environment, +# so this can be configured from a file or env vars. +dotenv.load_dotenv() + +# Get these values from console.helium.com under Device Details. +key_path = os.environ.get('key_path', 'keys.json') +if os.path.exists(key_path): + contents = open(key_path).read() + if contents: + filekeys = json.loads(contents) + else: + filekeys = {} +else: + filekeys = {} + +deveui = bytes.fromhex(os.environ.get('deveui', filekeys.get('deveui', ''))) +appeui = bytes.fromhex(os.environ.get('appeui', filekeys.get('appeui', ''))) +appkey = bytes.fromhex(os.environ.get('appkey', filekeys.get('appkey', ''))) + +# Fill in these values when you activate the device with otaa_helium.py. +devaddr = json.loads(os.environ.get('devaddr', filekeys.get('devaddr', '[]'))) +nwskey = json.loads(os.environ.get('nwskey', filekeys.get('nwskey', '[]'))) +appskey = json.loads(os.environ.get('appskey', filekeys.get('appskey', '[]'))) + +def get_keys(): + return { + "deveui": deveui, + "appeui": appeui, + "appkey": appkey, + "devaddr": devaddr, + "nwskey": nwskey, + "appskey": appskey, + + } + +def write(keys, path=key_path): + writable_keys = {} + for k,v in keys.items(): + if type(v) == bytes: + writable_keys[k] = keys[k].hex() + else: + writable_keys[k] = json.dumps(v) + f = open(path, 'w') + f.write(json.dumps(writable_keys)) + f.close() + \ No newline at end of file diff --git a/otaa_helium.py b/otaa_helium.py old mode 100644 new mode 100755 index a02701d..116269b --- a/otaa_helium.py +++ b/otaa_helium.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 + import sys from time import sleep from SX127x.LoRa import * @@ -8,7 +9,7 @@ from LoRaWAN.MHDR import MHDR from random import randrange import reset_ada -import helium +import helium_helper import keys BOARD.setup() @@ -45,13 +46,11 @@ def on_rx_done(self): def on_tx_done(self): self.clear_irq_flags(TxDone=1) print("TxDone") - - self.set_mode(MODE.SLEEP) self.set_dio_mapping([0,0,0,0,0,0]) self.set_invert_iq(1) self.reset_ptr_rx() - self.set_freq(helium.DOWNFREQ)#915) + self.set_freq(helium_helper.DOWNFREQ)#915) self.set_spreading_factor(7)#12) self.set_bw(9) #500Khz self.set_rx_crc(False)#TRUE @@ -59,8 +58,6 @@ def on_tx_done(self): def start(self): - self.tx_counter = 1 - lorawan = LoRaWAN.new(keys.appkey) lorawan.create(MHDR.JOIN_REQUEST, {'deveui': keys.deveui, 'appeui': keys.appeui, 'devnonce': devnonce}) @@ -79,7 +76,7 @@ def start(self): # Setup lora.set_mode(MODE.SLEEP) lora.set_dio_mapping([1,0,0,0,0,0]) -lora.set_freq(helium.UPFREQ) +lora.set_freq(helium_helper.UPFREQ) lora.set_pa_config(pa_select=1) lora.set_spreading_factor(7) lora.set_pa_config(max_power=0x0F, output_power=0x0E) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1deb752 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +pycryptodome +adafruit_blinka +adafruit-circuitpython-ssd1306 +python-dotenv +shortuuid +gpsdclient \ No newline at end of file diff --git a/rssi_helium.py b/rssi_helium.py old mode 100644 new mode 100755 index dc8554b..1efcbc3 --- a/rssi_helium.py +++ b/rssi_helium.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 + +import json import sys import argparse +import datetime from time import sleep from SX127x.LoRa import * from SX127x.LoRaArgumentParser import LoRaArgumentParser @@ -14,12 +17,14 @@ from digitalio import DigitalInOut, Direction, Pull import board import busio +import os +import re +import shortuuid import RPi.GPIO as GPIO import helium import keys -import frame # Button A btnA = DigitalInOut(board.D5) @@ -48,30 +53,33 @@ display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, reset=reset_pin) # Clear the display. display.fill(0) -display.text('LoRA!', 0, 0, 1) +display.text("Test is ready to start!", 0, 0, 1) +display.text('Push Left Button', 0, 10, 1) +display.text('To start test.', 0, 20, 1) display.show() width = display.width height = display.height -global msg -msg = 'Test' - class LoRaWANotaa(LoRa): - def __init__(self, verbose = False): + def __init__(self, verbose = False, ack=True, start_ping=False): super(LoRaWANotaa, self).__init__(verbose) - self.ack = False + self.iter = 0 + self.uuid = shortuuid.uuid() + self.ack = ack + self.test_status = {"running_ping": start_ping, "ping_count": 0, "last_message": None} + self.last_test = datetime.datetime.fromtimestamp(0) def on_rx_done(self): self.clear_irq_flags(RxDone=1) payload = self.read_payload(nocheck=True) print("Raw payload: {}".format(payload)) - lorawan = LoRaWAN.new(keys.nwskey, keys.appskey) lorawan.read(payload) decoded = "".join(list(map(chr, lorawan.get_payload()))) + self.test_status["last_message"] = decoded + self.test_status["ping_count"] += 1 print("Decoded: {}".format(decoded)) print("\n") - if lorawan.get_mhdr().get_mtype() == MHDR.UNCONF_DATA_DOWN: print("Unconfirmed data down.") downlink = decoded @@ -83,70 +91,78 @@ def on_rx_done(self): elif lorawan.get_mhdr().get_mtype() == MHDR.CONF_DATA_DOWN: print("Confirmed data down.") self.ack = True - downlink = decoded + downlink = decoded elif lorawan.get_mhdr().get_mtype() == MHDR.CONF_DATA_UP: print("Confirmed data up.") - downlink = decoded + downlink = decoded else: print("Other packet.") downlink = '' - - self.set_mode(MODE.STDBY) - s = '' - s += " pkt_snr_value %f\n" % self.get_pkt_snr_value() - s += " pkt_rssi_value %d\n" % self.get_pkt_rssi_value() - s += " rssi_value %d\n" % self.get_rssi_value() - s += " msg: %s" % downlink - display.fill(0) - display.text(s, 0, 0, 1) - display.show() + s += f" pkt_snr_value {self.get_pkt_snr_value():.2f}\n" + s += f" pkt_rssi_value {self.get_pkt_rssi_value():d}\n" + s += f" rssi_value {self.get_rssi_value():d}\n" + s += f" msg: {downlink}" print(s) def increment(self): self.tx_counter += 1 - data_file = open("frame.py", "w") - data_file.write( - 'frame = {}\n'.format(self.tx_counter)) + + data_file = open("frame.txt", "w") + data_file.write(f'frame = {self.tx_counter:d}\n') data_file.close() - def tx(self, conf=True): - global msg + def tx(self, msg, conf=False): if conf: data = MHDR.CONF_DATA_UP print('Sending confirmed data up.') else: data = MHDR.UNCONF_DATA_UP - print('Sending unconfirmed data up.') + print('Sending unconfirmed data up.') self.increment() lorawan = LoRaWAN.new(keys.nwskey, keys.appskey) + base = {'devaddr': keys.devaddr, 'fcnt': self.tx_counter, 'data': list(map(ord, msg))} if self.ack: print('Sending with Ack') - lorawan.create(data, {'devaddr': keys.devaddr, 'fcnt': self.tx_counter, 'data': list(map(ord, msg)), 'ack':True}) + lorawan.create(data, dict(**base, **{'ack':True})) self.ack = False else: print('Sending without Ack') - lorawan.create(data, {'devaddr': keys.devaddr, 'fcnt': self.tx_counter, 'data': list(map(ord, msg))}) - print("tx: {}".format(lorawan.to_raw())) + lorawan.create(data, base) + print(f"tx: {lorawan.to_raw()}") self.write_payload(lorawan.to_raw()) self.set_mode(MODE.TX) - display.fill(0) - display.text('Transmit!', 0, 0, 1) - display.show() - def start(self): + def start(self, msg): + package = json.dumps({"i": self.iter, "s": self.uuid, "m": msg}) self.setup_tx() - self.tx() while True: sleep(.1) - if not btnB.value: + display.fill(0) + display.text("Test is "+str(self.test_status["running_ping"]), 0, 0, 1) + display.text('Time: '+str(self.test_status["last_message"]), 0, 10, 1) + display.text('Total Pings: '+str(self.test_status["ping_count"]), 0, 20, 1) + display.show() + + if self.test_status["running_ping"] and (datetime.datetime.now() - self.last_test).seconds > 5: self.setup_tx() - self.tx() + print("Sending LoRaWAN tx with conf\n") + self.tx(package, conf=True) + self.iter = self.iter+1 + self.last_test = datetime.datetime.now() + if not btnA.value: + self.test_status["running_ping"] = True + if not btnB.value: + self.test_status["running_ping"] = False if not btnC.value: - self.setup_tx() - self.tx(False) + display.fill(0) + display.text("Test is shut down!", 0, 0, 1) + display.text('Must restart PI to', 0, 10, 1) + display.text('restart test.', 0, 20, 1) + display.show() + raise KeyboardInterrupt def set_frame(self,frame): self.tx_counter = frame @@ -163,30 +179,36 @@ def setup_tx(self): self.set_sync_word(0x34) self.set_rx_crc(True) self.set_invert_iq(0) - assert(self.get_agc_auto_on() == 1) + assert(self.get_agc_auto_on() == 1) def on_tx_done(self): self.clear_irq_flags(TxDone=1) self.set_mode(MODE.SLEEP) self.set_dio_mapping([0,0,0,0,0,0]) - self.set_freq(helium.DOWNFREQ) + self.set_freq(helium.DOWNFREQ) self.set_bw(9) - self.set_spreading_factor(7) + self.set_spreading_factor(7) self.set_pa_config(pa_select=1) self.set_sync_word(0x34) self.set_rx_crc(False) self.set_invert_iq(1) - self.reset_ptr_rx() self.set_mode(MODE.RXCONT) -def init(frame): - lora = LoRaWANotaa(False) +def init(msg=None, start_ping=False): + lora = LoRaWANotaa(False, start_ping=start_ping) + + frame = 0 + if os.path.exists('frame.txt'): + with open('frame.txt') as df: + for line in df: + if m := re.match('^frame\s*=\s*(\d+)', line): + frame = int(m.group(1)) + lora.set_frame(frame) try: - print("Sending LoRaWAN tx\n") - lora.start() + lora.start(msg) except KeyboardInterrupt: sys.stdout.flush() print("\nKeyboardInterrupt") @@ -197,16 +219,12 @@ def init(frame): def main(): - global frame - global msg - # parser = argparse.ArgumentParser(add_help=True, description="Trasnmit a LoRa msg") - # parser.add_argument("--frame", help="Message frame") - # parser.add_argument("--msg", help="tokens file") - # args = parser.parse_args() - # frame = int(args.frame) - # msg = args.msg - msg = 'Test' - init(frame.frame) + start_ping = False + if 'start' in sys.argv: + start_ping = True + if 'debug' in sys.argv: + import code;code.interact(local=dict(globals(), **locals())) + init(start_ping=start_ping) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/run.py b/run.py new file mode 100644 index 0000000..3db302b --- /dev/null +++ b/run.py @@ -0,0 +1,52 @@ +import datetime +import time +import json +from helium import Helium +from gps import get_gps_data, get_dist, should_send_gps +import logging +import sys + +logger = logging.getLogger() +logger.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s', + '%m-%d-%Y %H:%M:%S') +file_handler = logging.FileHandler('/home/pi/run.log') +file_handler.setLevel(logging.DEBUG) +file_handler.setFormatter(formatter) + +logger.addHandler(file_handler) +def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + +sys.excepthook = handle_exception + +def fire_ping(last_gps, last_sent_at): + logging.info('Last GPS was '+str(last_gps)) + gps_data = get_gps_data() + logging.info('Latest GPS is '+str(gps_data)) + response = {} + if should_send_gps(gps_data, last_gps, last_sent_at): + logging.info('Will send ping') + response = helium.transact(str(round(gps_data["lat"], 5))+","+str(round(gps_data["lon"], 5))+","+str(int(gps_data["speed"] or 0))) + if response == 10: + last_sent_at = datetime.datetime.now() + last_gps = gps_data + logging.info('Ping response was '+str(response)) + else: + logging.info('Wont send ping') + return response, last_gps, last_sent_at + +helium = Helium() +last_gps = {} +last_sent_at = None +while True: + try: + response, last_gps, last_sent_at = fire_ping(last_gps, last_sent_at) + time.sleep(int(response or "10")) + #time.sleep(response.get("next_ping_at", 10)) + except: + print("oops") + time.sleep(10)