Skip to content
Snippets Groups Projects
Pi_Receiver.py 10.5 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

DEBUG = 1
API_URL = "http://environmental-monitoring.int.4oh4.co/api"

BOARD.setup()

parser = LoRaArgumentParser("Continous LoRa receiver.")

# Recived from Sensors when they have data to send
class RxHello(object):
    def __init__(self, uid, reservationTime):
        self.uid = uid
        self.reservationTime = reservationTime
        self.okToTransmit = False
        self.txAuthID = None

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

    def setTxAuthID(self, txAuthID):
        self.txAuthID = txAuthID

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

class SensorResponse(object):
    def __init__(self, sensorMetadata, data):
        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, indent=4)

    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 > 0:
            print(response)
        return

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 Reply():
    ackStatus = False

    def __init__(self, remoteSensorID, replyMsgID):
        self.messageID = random.randrange(10000, 99999)  
        self.gatewayUid = getserial()
        self.remoteSensorID = remoteSensorID
        self.replyMsgID = replyMsgID

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

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

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)

    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(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)

# 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)

    print(data.ToJson())

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

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

    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(data):
    timeBegin = time()
    print("\n[?] Try RX Hello!")
    if DEBUG > 1:
        print("\nRaw data: %s" % data)

    try:
        rxHelloJson = json.loads(data)
        rxHello = RxHello(**rxHelloJson)

    except Exception as e:
        print("[-] Not RX Hello\n")
        return False

    print("[+] RX Hello Confirmed!\n")
    # TODO Check i'm not expecting any methods within rxHello.reservationTime seconds

    # Send "OK" + UID + TX_AUTH_ID
    rxHello.setOK(True) # We are happy for this sensor to send it's data
    rxHello.setTxAuthID(123)
    print(rxHello.ToJson())

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

    loraReceiver.transmit(payload)
    print("\n[+] RX Hello \"OK\" Reply Sent\n")

    # Sleep until out transmit block expires (2 Seconds)
    #sleep(2 - (time() - timeBegin))
    sleep(0.2)
    return True


# handle SensorResponse data packet
def handleSensorResponsePacket(data):
    timeBegin = time()
    print("\n[?] Try Sensor Response Data!\nRaw data: %s" % data)

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

    except Exception as e:
        print("\n[-] 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!\n")
    print(p.ToJson())

    # TODO Validate response is valid and non-corrupt

    # TODO Ack / process here

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

    # Process response
    print("Sensor ID: %s \tPPM 10: %s\n" % (p.sensorMetadata.uid, p.sensorReading.ppm.p10))

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

    # Sleep until out transmit block expires (2 Seconds)
    #sleep(2 - (time() - timeBegin))
    sleep(0.2)



# Parse LoRa response, validate, save / transmit
def handleData(data):
    # 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(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=1)
#loraReceiver.set_rx_crc(True)
#loraReceiver.set_coding_rate(CODING_RATE.CR4_6)
#loraReceiver.set_pa_config(max_power=0, output_power=0)
#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)

# Go Go Go!
print("[+] Receiver & API Gateway")
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()