diff --git a/test/Pi_Receiver.py b/test/Pi_Receiver.py
new file mode 100644
index 0000000000000000000000000000000000000000..6495104dde00af5606fe5adf6f36d90b03a791bc
--- /dev/null
+++ b/test/Pi_Receiver.py
@@ -0,0 +1,363 @@
+#!/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()