Skip to content
Snippets Groups Projects
Pi_Receiver.py 13.6 KiB
Newer Older
#!/usr/bin/env python3

'''
    =========================================================================================
 
        CS408 Environmental Monitoring Independent of Existing Infrastructure
        Copyright (C) 2021 Callum Inglis

        This program is free software; you can redistribute it and/or modify
        it under the terms of the GNU General Public License as published by
        the Free Software Foundation; either version 2 of the License, or
        (at your option) any later version.

        This program is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
        GNU General Public License for more details.

        You should have received a copy of the GNU General Public License along
        with this program; if not, write to the Free Software Foundation, Inc.,
        51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

        Contact: Callum.Inglis.2018(at)uni.strath.ac.uk

    =========================================================================================

    Libraries Used
        https://github.com/raspberrypi-tw/lora-sx1276

    =========================================================================================
'''

# Usage: python Pi_Receiver.py -f 433 -b BW125 -s 7

from argparse import ArgumentError
import sys 
sys.path.insert(0, '../')     

from time import sleep, time
import json
import requests # pip3 install requests
import random

from SX127x.LoRa import *
from SX127x.LoRaArgumentParser import LoRaArgumentParser
from SX127x.board_config import BOARD
import SX127x.packer as packer

Callum Inglis's avatar
Callum Inglis committed
import secrets

API_URL = "https://emiei.4oh4.co/api"
MAX_TX_RESERVATION_TIME = 2 # Seconds

BOARD.setup()

parser = LoRaArgumentParser("Continous LoRa receiver.")

# Recived from Sensors when they have data to send
class RxHello(object):
    def __init__(self, uid, reservationTime, messageID):
        self.uid = uid
        self.reservationTime = min(reservationTime, MAX_TX_RESERVATION_TIME)
        self.messageID = messageID
        self.gatewayUid = getserial()
        self.okTransmit = False

    def setOK(self, okToTransmit):
        self.okTransmit = okToTransmit

    # Python Object to JSON Object
    def ToJson(self):
        return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)

class SensorResponse(object):
    def __init__(self, sensorMetadata, data):
        self.gatewayMetadata = GatewayMetadata()
        self.sensorMetadata = SensorMetadata(**sensorMetadata)
        self.sensorReading = SensorReading(**data)

    # Python Object to JSON Object
    def ToJson(self):
        return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)

    def sendToApi(self):
        headers = {'Content-Type': 'Application/json'}
        response = requests.post(API_URL + '/sensor/reading', data=self.ToJson(), headers=headers)#, verify=False) # Using self signed cert for now, sort later!
        if DEBUG > 1:
Callum Inglis's avatar
Callum Inglis committed
            print(self.ToJson())
            print(response) # Ensure 200 Response!
        return

class GatewayMetadata(object):
Callum Inglis's avatar
Callum Inglis committed
    def __init__(self):
        self.gatewayUID = getserial()
        self.apiKey = secrets.GATEWAY_API_KEY
Callum Inglis's avatar
Callum Inglis committed

class SensorMetadata(object):
    def __init__(self, uid, messageID, samplePeriod):
        self.uid = uid
        self.messageID = messageID
        self.samplePeriod = samplePeriod
        self.sampleTime = round(time())

class SensorReading(object):
    def __init__(self, ppm, sht, co2):
        self.ppm = PPM(**ppm)
        self.sht = SHT(**sht)
        self.co2 = Co2(**co2)

class PPM(object):
    def __init__(self, p10, p25, p100):
        self.p10 = p10
        self.p25 = p25
        self.p100 = p100

class SHT(object):
    def __init__(self, temperature, humidity):
        self.temperature = temperature
        self.humidity = humidity

class Co2(object):
    def __init__(self, tmp):
        self.tmp = "Coming Soon!"

class API():
    def getSensorConfig(self, sensorUID):
        headers = {'Content-Type': 'Application/json'}
        response = requests.post(API_URL + '/api/' + secrets.API_KEY + '/sensor/getConfig/' + sensorUID, headers=headers)

        if (response.status_code != 200):
            return None

        return json.loads(response.content)

class Reply():
    ackStatus = False

    def __init__(self, remoteSensorID, replyMsgID):
        self.gatewayUid = getserial()
        self.uid = remoteSensorID
        self.replyMsgID = replyMsgID
        self.txAfterNReadings = None
        self.pollingFrequency = None

    def setAckStatus(self, ackStatus):
        self.ackStatus = ackStatus

    def updateConfigTxAfterNReadings(self, _nReadings):
        self.txAfterNReadings = _nReadings

    # Provide value in ms
    def updateConfigPollingFrequency(self, _pollingFrequency):
        self.pollingFrequency = _pollingFrequency

    def ToJson(self):
        return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)

class LoRaReceiver(LoRa):
    def __init__(self, verbose=False):
        super(LoRaReceiver, self).__init__(verbose)
        self._id = "Base-01"
        self.set_mode(MODE.SLEEP)
        self.set_dio_mapping([0] * 6)
        self.rxHelloQueue = RxHelloQueue() # Track transmission requests

    def on_rx_done(self):
        ###if DEBUG > 1:
        ###    print("\n[+] Rx Done")

        self.clear_irq_flags(RxDone=1)
        payload = self.read_payload(nocheck=True)

        # Parse our data here!
        data = ''.join([chr(c) for c in payload])
        handleData(self.rxHelloQueue, data)

        self.reset_ptr_rx()
        self.set_mode(MODE.RXCONT)

    def on_tx_done(self):
        print("\nTxDone")
        # set RX
        self.set_dio_mapping([0,0,0,0,0,0])    # RX
        sleep(1)
        self.reset_ptr_rx()
        self.set_mode(MODE.RXCONT)
        self.clear_irq_flags(RxDone=1)

    def on_cad_done(self):
        print("\non_CadDone")
        print(self.get_irq_flags())

    def on_rx_timeout(self):
        print("\non_RxTimeout")
        print(self.get_irq_flags())

    def on_valid_header(self):
        print("\non_ValidHeader")
        print(self.get_irq_flags())

    def on_payload_crc_error(self):
        print("\non_PayloadCrcError")
        print(self.get_irq_flags())

    def on_fhss_change_channel(self):
        print("\non_FhssChangeChannel")
        print(self.get_irq_flags())

    def receive(self):
        self.reset_ptr_rx()
        self.set_mode(MODE.RXCONT)
        while True:
            if (self.get_mode() == MODE.TX):
                rssi_value = self.get_rssi_value()
                status = self.get_modem_status()

                # if DEBUG > 1:
                sys.stdout.flush()
                sys.stdout.write("\r%d %d %d" % (rssi_value, status['rx_ongoing'], status['modem_clear']))

    def transmit(self, _payload):
        global args
        self.tx_counter = 0
        self.write_payload(_payload)
        self.set_mode(MODE.TX)


class RxHelloQueue():
    def __init__(self):
        self.reserved = False
        self.sensorID = None
        self.reservedUntil = None


    def hasReservationExpired(self):
        print("Time Now: ", time(), ", Reserved Until: ", self.reservedUntil)
        if self.reserved and self.reservedUntil < time():
            self.reserved = False
            return True
        
        return False


    def canAccept(self):
        self.hasReservationExpired()

        if not self.reserved:
            return True

        # TODO Additional Logic Here!
        return False


    def acceptNew(self, sensorID, reservationDuration):
        if not self.canAccept:
            return False
        
        self.reserved = True
        self.sensorID = sensorID
        self.reservedUntil = time() + reservationDuration + 1

# Source: https://www.raspberrypi-spy.co.uk/2012/09/getting-your-raspberry-pi-serial-number-using-python/
def getserial():
  # Extract serial from cpuinfo file
  cpuserial = "0000000000000000"
  try:
    f = open('/proc/cpuinfo','r')
    for line in f:
      if line[0:6]=='Serial':
        cpuserial = line[10:26]
    f.close()
  except:
    cpuserial = "ERROR000000000"
 
  return cpuserial

# Upon succesfully receiving a message, send back an ack
def ackMsg(sensorResponse):
    data = Reply(sensorResponse.sensorMetadata.uid, sensorResponse.sensorMetadata.messageID)
    data.setAckStatus(True)

    # Retrieve up-to-date sensor config from API & Transmit back to sensor
    updated_sensor_config = API().getSensorConfig(sensorResponse.sensorMetadata.uid)
    data.updateConfigPollingFrequency(updated_sensor_config['txAfterNReadings'])
    data.updateConfigTxAfterNReadings(updated_sensor_config['pollingFrequency'])

    if DEBUG > 1:
        print(data.ToJson())

    _length, _payload = packer.Pack_Str( data.ToJson() )

    payload = [int(hex(c), 0) for c in _payload]

    sleep(0.2) # Slight delay before transmitting, else too quick for sensor to start listening
    loraReceiver.transmit(payload)
    return

# When a sensor has data to transmit, it will send a TxHello. 
# Pick up that message here and decide if we can accept the message at this time.
def handleRxHello(rxHelloQueue, data):
    timeBegin = time()
    print("\n[?] Check for RX Hello")
    if DEBUG > 1:
        print("\nRaw data: %s" % data)

    try:
        rxHelloJson = json.loads(data)
        rxHello = RxHello(**rxHelloJson)
        print("    RX Hello Confirmed!")

    except Exception as e:
        print("    Not RX Hello")
        return False

    # TODO Check i'm not expecting any methods within rxHello.reservationTime seconds
    if not rxHelloQueue.canAccept():
        rxHello.setOK(False)
        print("[-] RX Hello \"No\" Reply Sent - Reserved by %s for %i seconds" % (rxHelloQueue.sensorID,  rxHelloQueue.reservedUntil - time()))
    
    else:
        rxHelloQueue.acceptNew(rxHello.uid, rxHello.reservationTime)

        # Send "OK" + UID
        rxHello.setOK(True) # We are happy for this sensor to send it's data
        print("[+] RX Hello \"OK\" Reply Sent - %s is permitted to send for %d seconds" % (rxHello.uid, rxHello.reservationTime))
    
    if DEBUG > 1:
       print(rxHello.ToJson())

    _length, _payload = packer.Pack_Str( rxHello.ToJson() ) # Send OK Back
    payload = [int(hex(c), 0) for c in _payload]

    sleep(0.2) # Slight delay before transmitting, else too quick for sensor to start listening
    loraReceiver.transmit(payload)
    sleep(0.2)
    return True


# handle SensorResponse data packet
def handleSensorResponsePacket(data):
    timeBegin = time()
    print("\n[?] Check for Sensor Response Data")
    if DEBUG > 1:
        print("\nRaw data: %s" % data)

    try:
        parsed = json.loads(data)
        p = SensorResponse(**parsed)

    except Exception as e:
        print("[-] Unable to Parse response, ignoring") # TODO Error handling, log increased error rates etc
        if DEBUG > 1:
            print("\tE: %e" % e)
        return False

    print("    Sensor Response Data Confirmed!")
    
    if DEBUG > 1:
        print("    Received Payload: %s" % p.ToJson())

    # TODO Validate response is valid and non-corrupt

    # TODO Ack / process here

    # TODO Transmit ACK
    print("    Sending Sensor Response Ack")
    ackMsg(p)
    print("[+] Sensor Response Ack Sent")

    # TODO Process response?

    # TODO Send To API
    print("    Sending Data to API")
    p.sendToApi()
    print("[+] Data Sent to API")

    sleep(0.2)
    return True



# Parse LoRa response, validate, save / transmit
def handleData(rxHelloQueue, data):
    print("\n===================\n[i] Packet Incoming")

    # Handle TxHello Transmission
    #       Rx - "I've got data to send!" + UID (From Sensor)
    #       If not expecting any messages, continue
    #       Tx - "OK" + UID (Back to Sensor) + TX_Auth_ID
    #
    #       <Do not Tx for 2s>
    if (handleRxHello(rxHelloQueue, data)): # Handled all OK
        return

    # Handle Payload Transmission
    #       Rx - Packet + UID + Tx_Auth_ID (From Sensor)
    #       Handle message, ack / nak appropriately
    #       Tx - Ack + UID + Next_Send_Interval
    #       Tx - Nak + UID
    #
    #       <Do not Tx for 3s>
    if (handleSensorResponsePacket(data)):
        return
        

    # T+3   Tx reserved window expires


# Setup Receiver
loraReceiver = LoRaReceiver(verbose=False)
args = parser.parse_args(loraReceiver)

loraReceiver.set_mode(MODE.STDBY)
loraReceiver.set_pa_config(pa_select=PA_SELECT.PA_BOOST)
#loraReceiver.set_rx_crc(True)
#loraReceiver.set_coding_rate(CODING_RATE.CR4_6)
loraReceiver.set_pa_config(max_power=300, output_power=500) # Default max_power = 10.8dB, output_power  = 4.2dB
#loraReceiver.set_lna_gain(GAIN.G1)
#loraReceiver.set_implicit_header_mode(False)
#loraReceiver.set_low_data_rate_optim(True)
#loraReceiver.set_pa_ramp(PA_RAMP.RAMP_50_us)
#loraReceiver.set_agc_auto_on(True)


print("Power:")
print(loraReceiver.get_pa_config(convert_dBm=True))

# Go Go Go!
print("[+] Receiver & API Gateway")
loraReceiver.set_agc_auto_on(1)
assert(loraReceiver.get_agc_auto_on() == 1)

try:
    loraReceiver.receive()

except KeyboardInterrupt:
    sys.stdout.flush()
    print("")
    sys.stderr.write("KeyboardInterrupt\n")

finally:
    sys.stdout.flush()
    print("")
    loraReceiver.set_mode(MODE.SLEEP)
    BOARD.teardown()