diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..4ac85996 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +# ignore everything +* + +# except the hardware directory +!hardware + +# things we want to ignore in the file directory +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.pyd +**/.Python +**/.env +**/pip-log.txt +**/pip-delete-this-directory.txt +**/.tox +**/.coverage +**/.coveragerc +**/.coverage.* +**/.cache +**/nosetests.xml +**/coverage.xml +**/*,cover +**/*.log + +# things we want to specifically include +!pi_requirements.txt +!Dockerfile + diff --git a/.flake8 b/.flake8 index a79ff2cb..da23d555 100644 --- a/.flake8 +++ b/.flake8 @@ -3,5 +3,6 @@ max-line-length = 119 exclude = .git, __pycache__, - venv + venv, + pi_venv **/migrations diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..53ef182b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## Title +_Give a self-explanatory title_ + +## Description +_If this fixes a bug or resolves an issue, provide a link to that issue. Give a detailed description that answers:_ +- _what is the purpose of this PR?_ +- _what is the original vs the new behaviour?_ +- _what bug does this PR attempt to fix?_ +- _what is documented in the new documentation?_ + +## Types of Changes +_Put an `x` in the boxes that apply_ + +- [ ] Feature (non-breaking change which adds functionality) +- [ ] Bug Fix (non-breaking change that fixes an issue) +- [ ] Breaking Change (feature/fix that causes existing features to not work as expected) +- [ ] Documentation + +## Checklist + +- [ ] I have read the [contribute]contributing doc +- [ ] Classes, scripts, and environment variables follow existing naming convention +- [ ] Lint and Unit tests pass locally +- [ ] New features on hardware have been tested on a local Raspberry Pi +- [ ] Mention new programs/binaries if any must be installed along with this change +- [ ] Mention new environment variables if any have been added to hardware/env file +- [ ] Test coverage should not drop more than 3% \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 47156af2..4742aca4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,18 +62,18 @@ jobs: services: - docker - before_script: + install: - docker pull nyumotorsportstelemetryorg/mercury || true - - script: # prepare qemu for arm emulation on x64 hardware - docker run --rm --privileged multiarch/qemu-user-static:register --reset # build image - # - docker build -t gcivil-nyu-org/spring2020-cs-gy-9223-class . - docker build --pull --cache-from nyumotorsportstelemetryorg/mercury --tag nyumotorsportstelemetryorg/mercury . - # basic help world test to see if it's working - - docker run nyumotorsportstelemetryorg/mercury grep -q "Hello, Docker!" hello.txt - # - black --check --exclude "migrations/" hardware + - docker run -d --name test_pi nyumotorsportstelemetryorg/mercury + + script: + # insure that the server started up as expected + - docker ps -a + - docker ps | grep -q test_pi after_script: - docker images diff --git a/Dockerfile b/Dockerfile index cb76b1cb..12a7359f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,28 @@ FROM raspbian/stretch +ENV PYTHONUNBUFFERED 1 + # install common build dependencies and clean up afterwards RUN apt-get update && apt-get install -y --no-install-recommends \ raspi-config \ + python3-pip \ + python3-sense-emu \ + sense-emu-tools \ && rm -rf /var/lib/apt/lists/* +RUN mkdir ~/Downloads +RUN mkdir hardware + # copy setup scripts -COPY ./hardware . +COPY ./hardware/setup/raspberrypi-common.sh . # run setup -RUN bash ./setup/raspberrypi-common.sh +RUN bash ./raspberrypi-common.sh + +COPY ./hardware/pi_requirements.txt . +RUN sudo python3 -m pip install pip --upgrade --force +RUN sudo pip3 install -r pi_requirements.txt + +COPY ./hardware hardware/ -RUN echo "Hello, Docker!" > hello.txt \ No newline at end of file +CMD [ "python3", "-m", "hardware.main" ] \ No newline at end of file diff --git a/hardware/CommunicationsPi/comm_pi.py b/hardware/CommunicationsPi/comm_pi.py index 36968e0b..a4f5da00 100644 --- a/hardware/CommunicationsPi/comm_pi.py +++ b/hardware/CommunicationsPi/comm_pi.py @@ -1,10 +1,13 @@ +import os from http.server import BaseHTTPRequestHandler from hardware.CommunicationsPi.radio_transceiver import Transceiver -transceiver = Transceiver() - class CommPi(BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.transceiver = Transceiver() + super().__init__(*args, **kwargs) + def _set_response(self): self.send_response(200) self.send_header("Content-type", "text/html") @@ -23,5 +26,9 @@ def do_POST(self): self.wfile.write("POST request for {}".format(self.path).encode("utf-8")) def send_data(self, payload): - transceiver.send(payload) + if os.environ.get("ENABLE_INTERNET_TRANSMISSION"): + print("transmit via internet") + if os.environ.get("ENABLE_RADIO_TRANSMISSION"): + print("transmit via radio") + self.transceiver.send(payload) return diff --git a/hardware/CommunicationsPi/find_port.py b/hardware/CommunicationsPi/find_port.py deleted file mode 100644 index 92ceb2e0..00000000 --- a/hardware/CommunicationsPi/find_port.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -import sys -import argparse -import serial -import serial.tools.list_ports - -from hardware.Utils.utils import get_logger - - -def is_usb_serial(port, args): - if port.vid is None: - return False - if hasattr(args, "vid") and args.vid is not None: - if port.vid is not args.vid: - return False - if hasattr(args, "pid") and args.pid is not None: - if port.pid is not args.pid: - return False - if hasattr(args, "vendor") and args.vendor is not None: - if not port.manufacturer.startswith(args.vendor): - return False - if hasattr(args, "serial") and args.serial is not None: - if not port.serial_number.startswith(args.serial): - return False - if hasattr(args, "intf") and args.intf is not None: - if port.interface is None or args.intf not in port.interface: - return False - return True - - -def extra_info(port): - extra_items = [] - if port.manufacturer: - extra_items.append("vendor '{}'".format(port.manufacturer)) - if port.serial_number: - extra_items.append("serial '{}'".format(port.serial_number)) - if port.interface: - extra_items.append("intf '{}'".format(port.interface)) - if extra_items: - return " with " + " ".join(extra_items) - return "" - - -def get_port(): - for port in serial.tools.list_ports.comports(): - if is_usb_serial(port, None): - return "port found" - return - - -def main(): - """The main program.""" - parser = argparse.ArgumentParser( - prog="find-port.py", - usage="%(prog)s [options] [command]", - description="Find the /dev/tty port for a USB Serial devices", - ) - parser.add_argument( - "-l", - "--list", - dest="list", - action="store_true", - help="List USB Serial devices currently connected", - ) - parser.add_argument( - "-s", - "--serial", - dest="serial", - help="Only show devices with the indicated serial number", - default=None, - ) - parser.add_argument( - "-n", - "--vendor", - dest="vendor", - help="Only show devices with the indicated vendor name", - default=None, - ) - parser.add_argument( - "--pid", - dest="pid", - action="store", - help="Only show device with indicated PID", - default=None, - ) - parser.add_argument( - "-v", - "--verbose", - dest="verbose", - action="store_true", - help="Turn on verbose messages", - default=False, - ) - parser.add_argument( - "--vid", - dest="vid", - action="store", - help="Only show device with indicated VID", - default=None, - ) - parser.add_argument( - "-i", - "--intf", - dest="intf", - action="store", - help="Shows devices which conatin the indicated interface string", - default=None, - ) - args = parser.parse_args(sys.argv[1:]) - - logger = get_logger("PORT_LOGGER", file_name="PORT_LOG_FILE") - - if args.verbose: - logger.info("pyserial version = {}".format(serial.__version__)) - logger.info(f" vid = {args.vid}") - logger.info(f" pid = {args.pid}") - logger.info(f"serial = {args.serial}") - logger.info(f"vendor = {args.vendor}") - - if args.list: - detected = False - for port in serial.tools.list_ports.comports(): - if is_usb_serial(port, args): - logger.info( - "USB Serial Device {:04x}:{:04x}{} found @{}\r".format( - port.vid, port.pid, extra_info(port), port.device - ) - ) - detected = True - if not detected: - logger.warn("No USB Serial devices detected.\r") - return - - for port in serial.tools.list_ports.comports(): - if is_usb_serial(port, args): - logger.info(port) - logger.info(port.device) - return - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/hardware/CommunicationsPi/lan_server.py b/hardware/CommunicationsPi/lan_server.py index 6877c17a..8d1ed695 100644 --- a/hardware/CommunicationsPi/lan_server.py +++ b/hardware/CommunicationsPi/lan_server.py @@ -3,19 +3,20 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from hardware.Utils.utils import get_logger -log = get_logger("LAN_SERVER_LOG_FILE") - class Server(BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.log = get_logger("LAN_SERVER_LOG_FILE") + super().__init__(*args, **kwargs) + def _set_response(self): self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() def do_GET(self): - global log - log.info( + self.log.info( "GET request,\nPath: %s\nHeaders:\n%s\n" + str(self.path) + str(self.headers) @@ -24,19 +25,18 @@ def do_GET(self): self.wfile.write("GET request for {}".format(self.path).encode("utf-8")) def do_POST(self): - global log content_length = int( self.headers["Content-Length"] ) # <--- Gets the size of data post_data = self.rfile.read(content_length) # <--- Gets the data itself - log.info( + self.log.info( "POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n" + str(self.path) + str(self.headers) + post_data.decode("utf-8") ) - log.info("data: " + str(post_data)) + self.log.info("data: " + str(post_data)) self._set_response() self.wfile.write("POST request for {}".format(self.path).encode("utf-8")) @@ -45,11 +45,10 @@ def do_POST(self): def runServer( server_class=HTTPServer, handler_class=Server, log_file_name=None, port=None ): - global log log = ( get_logger("LAN_SERVER_LOG_FILE") if log_file_name is None - else get_logger(log_file_name, log_file_name) + else get_logger(log_file_name) ) port = int(os.environ["LAN_PORT"]) if port is None else port diff --git a/hardware/CommunicationsPi/port.py b/hardware/CommunicationsPi/port.py deleted file mode 100644 index 49f3d17a..00000000 --- a/hardware/CommunicationsPi/port.py +++ /dev/null @@ -1,50 +0,0 @@ -# Test code written by Xiaofeng Xu to read serial messages from port - -import asyncio -import glob -import sys -import json -import serial -import serial_asyncio - - -class AsyncSerialProtocol(asyncio.Protocol): - def connection_made(self, transport): - self.transport = transport - print("port opened", transport) - transport.serial.rts = False - transport.write(b"hello world\n") - - def data_received(self, data): - m = data.decode("utf-8") - print(m) - # print("data received", repr(data)) - try: - message = json.loads(data.decode("utf-8")) - print(message) - except json.JSONDecodeError: - print() - - self.transport.close() - - def connection_lost(self, exc): - print("port closed") - asyncio.get_event_loop().stop() - - -ser = serial.Serial() -ports = glob.glob("/dev/tty.u*") -print(ports[0]) -ser.port = ports[0] -ser.timeout = 1 -ser.open() - -loop = asyncio.new_event_loop() -coro = serial_asyncio.create_serial_connection(loop, AsyncSerialProtocol, ports[0]) -try: - loop.run_until_complete(coro) - loop.run_forever() -except KeyboardInterrupt: - sys.stdout.write("\n") -finally: - loop.close() diff --git a/hardware/CommunicationsPi/radio_transceiver.py b/hardware/CommunicationsPi/radio_transceiver.py index 5c17baa9..2ec4c67c 100644 --- a/hardware/CommunicationsPi/radio_transceiver.py +++ b/hardware/CommunicationsPi/radio_transceiver.py @@ -31,18 +31,24 @@ def __init__(self, log_file_name=None, port=None): ), {}, ) - self.port_vid = port_info.vid - self.port_pid = port_info.pid - self.port_vendor = port_info.manufacturer - self.port_intf = port_info.interface - self.port_serial_number = port_info.serial_number + self.port_vid = port_info.vid if hasattr(port_info, "vid") else None + self.port_pid = port_info.pid if hasattr(port_info, "pid") else None + self.port_vendor = ( + port_info.manufacturer if hasattr(port_info, "manufacturer") else None + ) + self.port_intf = ( + port_info.interface if hasattr(port_info, "interface") else None + ) + self.port_serial_number = ( + port_info.serial_number if hasattr(port_info, "serial_number") else None + ) self.find_port() baudrate = os.environ["TRANSCEIVER_BAUDRATE"] parity = serial.PARITY_NONE stopbits = serial.STOPBITS_ONE bytesize = serial.EIGHTBITS - timeout = os.environ["TRANSCEIVER_TIMEOUT"] + timeout = int(os.environ["TRANSCEIVER_TIMEOUT"]) self.logging.info("Opening serial on: " + str(self.port)) self.serial = serial.Serial( diff --git a/hardware/CommunicationsPi/lan_client.py b/hardware/CommunicationsPi/web_client.py similarity index 71% rename from hardware/CommunicationsPi/lan_client.py rename to hardware/CommunicationsPi/web_client.py index 5e5b9476..a2ca01f1 100644 --- a/hardware/CommunicationsPi/lan_client.py +++ b/hardware/CommunicationsPi/web_client.py @@ -1,24 +1,23 @@ import os -import json import requests from requests.exceptions import HTTPError from hardware.Utils.utils import get_logger -class LANClient: - def __init__(self, log_file_name=None, lan_server_url=None): +class WebClient: + def __init__(self, log_file_name=None, server_url=None): if log_file_name is None: - self.logging = get_logger("LAN_CLIENT_LOG_FILE") + self.logging = get_logger("WEB_CLIENT_LOG_FILE") else: self.logging = get_logger(log_file_name, log_file_name) - if lan_server_url is None: + if server_url is None: self.url = self.get_server_url_from_env() else: - self.url = lan_server_url + self.url = server_url def get_server_url_from_env(self): - protocol = "https" if os.environ["LAN_SERVER_HTTPS"] else "http" + protocol = "https" if os.environ.get("LAN_SERVER_HTTPS") else "http" ip = os.environ["LAN_SERVER_IP"] port = os.environ["LAN_PORT"] @@ -33,8 +32,8 @@ def ping_lan_server(self, payload): self.logging.info("Pinging") try: - self.logging.info("data: " + json.dumps(payload)) - response = requests.post(self.url, data=json.dumps(payload)) + self.logging.info("data: " + payload) + response = requests.post(self.url, data=payload) response.raise_for_status() return response @@ -46,4 +45,3 @@ def ping_lan_server(self, payload): except Exception as err: self.logging.error("error occurred: {}".format(str(err))) raise - return diff --git a/hardware/SensorPi/main.py b/hardware/SensorPi/main.py deleted file mode 100644 index 1620cf3e..00000000 --- a/hardware/SensorPi/main.py +++ /dev/null @@ -1,27 +0,0 @@ -from sense_hat import SenseHat -import requests -import os -from sensehat_reader import temperature, pressure, humidity, acceleration, orientation - -# import json - -TEST_ENDPOINT = os.environ["TEST_ENDPOINT"] -API_ENDPOINT = os.environ["API_ENDPOINT"] - -sense = SenseHat() -nyu_purple = (87, 46, 140) -sense.show_message("MERCURY", text_colour=nyu_purple, scroll_speed=0.04) -sense.clear() - -while 1: - json_temperature = temperature() - json_pressure = pressure() - json_humidity = humidity() - json_acceleration = acceleration() - json_orientation = orientation() - - response_temperature = requests.post(url=API_ENDPOINT, data=json_temperature) - response_pressure = requests.post(url=API_ENDPOINT, data=json_pressure) - response_humidity = requests.post(url=API_ENDPOINT, data=json_humidity) - response_acceleration = requests.post(url=API_ENDPOINT, data=json_acceleration) - response_orientation = requests.post(url=API_ENDPOINT, data=json_orientation) diff --git a/hardware/SensorPi/sense_pi.py b/hardware/SensorPi/sense_pi.py index d3c3a23c..a77a7a00 100644 --- a/hardware/SensorPi/sense_pi.py +++ b/hardware/SensorPi/sense_pi.py @@ -1,6 +1,9 @@ import os -from datetime import datetime -from hardware.Utils.utils import get_logger, get_sensor_keys +from hardware.Utils.utils import ( + get_logger, + get_sensor_keys, + date_str_with_current_timezone, +) # Conditional import for sense hat and emulator try: @@ -57,7 +60,7 @@ def factory(self, type): data["values"] = {} data["values"][key] = sensor_data[key] - data["date"] = str(datetime.now()) + data["date"] = date_str_with_current_timezone() return data def get_all(self): diff --git a/hardware/SensorPi/sensehat_reader.py b/hardware/SensorPi/sensehat_reader.py deleted file mode 100644 index 5376c3e8..00000000 --- a/hardware/SensorPi/sensehat_reader.py +++ /dev/null @@ -1,77 +0,0 @@ -from sense_hat import SenseHat -import json -from datetime import datetime - -TEMPERATURE_ID = 6 -PRESSURE_ID = 7 -HUMIDITY_ID = 10 -ACCELERATION_ID = 8 -ORIENTATION_ID = 9 - - -def temperature(): - sense = SenseHat() - temperature = sense.get_temperature() - date = str(datetime.now()) - data = {} - data["sensor_id"] = TEMPERATURE_ID - data["values"] = {"temperature": temperature} - data["values"] = json.dumps(data["values"]) - data["date"] = date - return data - - -def pressure(): - sense = SenseHat() - pressure = sense.get_pressure() - date = str(datetime.now()) - data = {} - data["sensor_id"] = PRESSURE_ID - data["values"] = {"pressure": pressure} - data["values"] = json.dumps(data["values"]) - data["date"] = date - return data - - -def humidity(): - sense = SenseHat() - humidity = sense.get_humidity() - date = str(datetime.now()) - data = {} - data["sensor_id"] = HUMIDITY_ID - data["values"] = {"humidity": humidity} - data["values"] = json.dumps(data["values"]) - data["date"] = date - return data - - -def acceleration(): - sense = SenseHat() - acceleration = sense.get_accelerometer_raw() - date = str(datetime.now()) - data = {} - data["sensor_id"] = ACCELERATION_ID - data["values"] = { - "x": acceleration["x"], - "y": acceleration["y"], - "z": acceleration["z"], - } - data["values"] = json.dumps(data["values"]) - data["date"] = date - return data - - -def orientation(): - sense = SenseHat() - orientation = sense.get_orientation() - date = str(datetime.now()) - data = {} - data["sensor_id"] = ORIENTATION_ID - data["values"] = { - "roll": orientation["roll"], - "pitch": orientation["pitch"], - "yaw": orientation["yaw"], - } - data["values"] = json.dumps(data["values"]) - data["date"] = date - return data diff --git a/hardware/Utils/logger.py b/hardware/Utils/logger.py index 00dec5e9..f5c7bbf6 100644 --- a/hardware/Utils/logger.py +++ b/hardware/Utils/logger.py @@ -30,7 +30,7 @@ def __init__( self.logger.addHandler(self.console_logger) # show logs on screen - self.showLogsOnScreen = show_logs + self.showLogsOnScreen = show_logs or os.environ.get("SHOW_LOGS") def get_logger_file(self, file_name): d = os.environ["LOG_DIRECTORY"] diff --git a/hardware/Utils/utils.py b/hardware/Utils/utils.py index 18af3909..7fe53ee8 100644 --- a/hardware/Utils/utils.py +++ b/hardware/Utils/utils.py @@ -1,5 +1,7 @@ import os import json +from datetime import datetime, timezone + from .logger import Logger SENSOR_KEYS = { @@ -25,3 +27,7 @@ def get_logger(key, file_name=None): file_name = key logger = Logger(name=key, filename=os.environ[file_name]) return logger + + +def date_str_with_current_timezone(): + return datetime.now(timezone.utc).astimezone().isoformat() diff --git a/hardware/env b/hardware/env index c090a32e..38a317fc 100644 --- a/hardware/env +++ b/hardware/env @@ -1,20 +1,26 @@ -PI_TYPE=commPi -RADIO_TRANSMITTER_PORT=/dev/ttyUSB0 -RADIO_RECEIVER_PORT=/dev/ttyUSB0 -LAN_SERVER_HTTPS=False +HARDWARE_TYPE=commPi + +ENABLE_INTERNET_TRANSMISSION=False +LAN_SERVER_HTTPS= LAN_SERVER_IP=169.254.187.204 LAN_PORT=3322 +ENABLE_RADIO_TRANSMISSION=True +RADIO_TRANSMITTER_PORT=/dev/ttyUSB0 +RADIO_RECEIVER_PORT=/dev/ttyUSB0 EMULATE_SENSE_HAT=True +GPS_BAUDRATE=9600 +GPS_PORT=/dev/serial0 +TRANSCEIVER_BAUDRATE=9600 +TRANSCEIVER_TIMEOUT=1 + +DJANGO_SERVER_API_ENDPOINT=http://127.0.0.1:8080/measurement/ + +SHOW_LOGS=True LOG_DIRECTORY=logs TRANSMITTER_LOG_FILE=transmitter.log RECEIVER_LOG_FILE=receiver.log LAN_SERVER_LOG_FILE=lan_server.log -LAN_CLIENT_LOG_FILE=lan_client.log +WEB_CLIENT_LOG_FILE=web_client.log PORT_LOG_FILE=port.log SENSE_HAT_LOG_FILE=sense_hat.log - -TEST_ENDPOINT=http://pastebin.com/api/api_post.php -API_ENDPOINT=http://mecury-backend-prod.herokuapp.com/measurement/ - -TRANSCEIVER_BAUDRATE=9600 -TRANSCEIVER_TIMEOUT=1 +GPS_LOG_FILE=gps_reader.log diff --git a/hardware/gpsPi/__init__.py b/hardware/gpsPi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hardware/gpsPi/gps-serial.sh b/hardware/gpsPi/gps-serial.sh new file mode 100644 index 00000000..66c453e0 --- /dev/null +++ b/hardware/gpsPi/gps-serial.sh @@ -0,0 +1,2 @@ +stty -F /dev/serial0 raw 9600 cs8 clocal -cstopb +cat /dev/serial0 \ No newline at end of file diff --git a/hardware/gpsPi/gps_reader.py b/hardware/gpsPi/gps_reader.py new file mode 100644 index 00000000..e884f6bc --- /dev/null +++ b/hardware/gpsPi/gps_reader.py @@ -0,0 +1,59 @@ +import os +import serial +from ..Utils.utils import get_logger +from hardware.Utils.utils import date_str_with_current_timezone + +GPS_ID = 10 + + +class GPSReader: + def __init__(self, log_file_name=None): + self.gps = serial.Serial(os.environ["GPS_PORT"], os.environ["GPS_BAUDRATE"]) + + if log_file_name is None: + self.logging = get_logger("GPS_LOG_FILE") + else: + self.logging = get_logger(log_file_name, log_file_name) + + def get_geolocation(self): + while self.gps.inWaiting() == 0: + pass + + nmeaSentence = self.gps.readline().split(",") + nmeaType = nmeaSentence[0] + + # Added additional check to verify if nmeaSentence has valid data + if nmeaType == "$GPRMC" and nmeaSentence[2] == "A": + latitude_hours = float(nmeaSentence[3][0:2]) + latitude_minutes = float(nmeaSentence[3][2:]) + longitude_hours = float(nmeaSentence[5][0:3]) + longitude_minutes = float(nmeaSentence[5][3:]) + + latitude_decimal = latitude_hours + latitude_minutes / 60 + longitude_decimal = longitude_hours + longitude_minutes / 60 + + latitude_dir = nmeaSentence[4] + longitude_dir = nmeaSentence[6] + + if latitude_dir == "S": + latitude_decimal = latitude_decimal * -1 + if longitude_dir == "W": + longitude_decimal = longitude_decimal * -1 + + self.logging.info("latitude_decimal: " + str(latitude_decimal)) + self.logging.info("longitude_decimal: " + str(longitude_decimal)) + + data = {} + data["sensor_id"] = GPS_ID + data["values"] = { + "latitude": latitude_decimal, + "longitude": longitude_decimal, + } + data["date"] = date_str_with_current_timezone() + return data + else: + return None + + # print(latitude_decimal) + # print(longitude_decimal) + # print("") diff --git a/hardware/gpsPi/gpsmon.sh b/hardware/gpsPi/gpsmon.sh new file mode 100644 index 00000000..ed45b268 --- /dev/null +++ b/hardware/gpsPi/gpsmon.sh @@ -0,0 +1,3 @@ +# run gpsmon +sudo gpsd /dev/ttyAMA0 -F /var/run/gpsd.sock +gpsmon /dev/serial0 \ No newline at end of file diff --git a/hardware/gpsPi/setup.sh b/hardware/gpsPi/setup.sh new file mode 100644 index 00000000..b4fdfe7b --- /dev/null +++ b/hardware/gpsPi/setup.sh @@ -0,0 +1,11 @@ +# get gpsd +sudo apt-get update +sudo apt-get install gpsd gpsd-clients python-gps + +# disable the gpsd systemd service +sudo systemctl stop gpsd.socket +sudo systemctl disable gpsd.socket + +# enable the gpsd systemd service +# sudo systemctl enable gpsd.socket +# sudo systemctl start gpsd.socket \ No newline at end of file diff --git a/hardware/main.py b/hardware/main.py index 4aec3c0c..e56a19c0 100644 --- a/hardware/main.py +++ b/hardware/main.py @@ -1,33 +1,75 @@ import os import time +import json -from hardware.CommunicationsPi.comm_pi import CommPi -from hardware.CommunicationsPi.lan_server import runServer -from hardware.CommunicationsPi.lan_client import LANClient -from hardware.SensorPi.sense_pi import SensePi +from dotenv import load_dotenv -if os.environ["PI_TYPE"] == "commPi": +PI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +dotenv_file = os.path.join(PI_DIR, "hardware/env") +if os.path.isfile(dotenv_file): # pragma: no cover + load_dotenv(dotenv_path=dotenv_file) +else: + print("dotenv_file was not a file") + +from hardware.CommunicationsPi.radio_transceiver import Transceiver # noqa: E402 +from hardware.CommunicationsPi.comm_pi import CommPi # noqa: E402 +from hardware.CommunicationsPi.lan_server import runServer # noqa: E402 +from hardware.CommunicationsPi.web_client import WebClient # noqa: E402 +from hardware.SensorPi.sense_pi import SensePi # noqa: E402 +from hardware.Utils.utils import get_sensor_keys # noqa: E402 +from hardware.gpsPi.gps_reader import GPSReader # noqa: E402 + + +if os.environ["HARDWARE_TYPE"] == "commPi": print("CommunicationsPi") runServer(handler_class=CommPi) -else: +elif os.environ["HARDWARE_TYPE"] == "sensePi": print("SensePi") - sensePi = SensePi() - client = LANClient() + sensor_keys = get_sensor_keys() + sensor_ids = {} + sensor_ids[sensor_keys["TEMPERATURE"]] = 2 + sensor_ids[sensor_keys["PRESSURE"]] = 3 + sensor_ids[sensor_keys["HUMIDITY"]] = 4 + sensor_ids[sensor_keys["ACCELERATION"]] = 5 + sensor_ids[sensor_keys["ORIENTATION"]] = 6 + sensePi = SensePi(sensor_ids=sensor_ids) + gpsPi = GPSReader() + client = WebClient() while True: + print("while true") temp = sensePi.get_temperature() pres = sensePi.get_pressure() hum = sensePi.get_humidity() acc = sensePi.get_acceleration() orie = sensePi.get_orientation() all = sensePi.get_all() + coords = gpsPi.get_geolocation() + + if coords is not None: + data = [temp, pres, hum, acc, orie, coords, all] + else: + data = [temp, pres, hum, acc, orie, all] - data = [temp, pres, hum, acc, orie, all] for i in data: - print(i) + payload = json.dumps(i) + print(payload) try: - client.ping_lan_server(i) + client.ping_lan_server(payload) except Exception as err: print("error occurred: {}".format(str(err))) raise time.sleep(1) +else: + print("Local Django Server") + transceiver = Transceiver() + url = os.environ.get("DJANGO_SERVER_API_ENDPOINT") + if url: + client = WebClient(server_url=url) + while True: + data = transceiver.listen() + if data: + print(data) + client.ping_lan_server(json.loads(data)) + else: + print("DJANGO_SERVER_API_ENDPOINT not set") diff --git a/hardware/pi_requirements.txt b/hardware/pi_requirements.txt new file mode 100644 index 00000000..01a8252e --- /dev/null +++ b/hardware/pi_requirements.txt @@ -0,0 +1,10 @@ +certifi==2020.4.5.1 +chardet==3.0.4 +future==0.18.2 +idna==2.9 +iso8601==0.1.12 +pyserial==3.4 +python-dotenv==0.13.0 +PyYAML==5.3.1 +requests==2.23.0 +urllib3==1.25.9 diff --git a/hardware/tests/test_comm_pi.py b/hardware/tests/test_comm_pi.py new file mode 100644 index 00000000..e982e589 --- /dev/null +++ b/hardware/tests/test_comm_pi.py @@ -0,0 +1,52 @@ +from django.test import SimpleTestCase +import os + +# from json.decoder import JSONDecodeError +from http.server import HTTPServer + +from unittest import mock +from unittest.mock import patch + +import threading +import socket +import requests + +from hardware.CommunicationsPi.comm_pi import CommPi + + +def get_free_port(): + s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) + s.bind(("localhost", 0)) + address, port = s.getsockname() + s.close() + return port + + +class CommPiTests(SimpleTestCase): + def setUp(self): + self.mock_server_port = get_free_port() + self.mock_server = HTTPServer(("localhost", self.mock_server_port), CommPi) + + self.mock_server_thread = threading.Thread( + target=self.mock_server.serve_forever + ) + self.mock_server_thread.setDaemon(True) + self.mock_server_thread.start() + + @mock.patch("hardware.CommunicationsPi.comm_pi.Transceiver") + def test_get(self, mock_transceiver=mock.MagicMock()): + url = f"http://localhost:{self.mock_server_port}/" + response = requests.get(url) + + self.assertTrue(response.ok) + self.assertTrue(response.headers.get("Content-Type") == "text/html") + + @mock.patch("hardware.CommunicationsPi.comm_pi.Transceiver") + def test_post_radio(self, mock_transceiver=mock.MagicMock()): + with patch.dict( + os.environ, {"ENABLE_RADIO_TRANSMISSION": "True"}, + ): + mock_transceiver.return_value.send = mock.MagicMock() + url = f"http://localhost:{self.mock_server_port}/" + requests.post(url, data={"key": "value"}, headers={"Content-Length": "15"}) + mock_transceiver.return_value.send.assert_called() diff --git a/hardware/tests/test_find_port.py b/hardware/tests/test_find_port.py deleted file mode 100644 index 4e6fca90..00000000 --- a/hardware/tests/test_find_port.py +++ /dev/null @@ -1,238 +0,0 @@ -from django.test import SimpleTestCase - -from unittest import mock -from serial.tools.list_ports_common import ListPortInfo -from argparse import Namespace - -from hardware.CommunicationsPi.find_port import is_usb_serial, extra_info, get_port - - -class IsUsbSerialTests(SimpleTestCase): - """ - Tests for the is_usb_serial function - """ - - def test_port_vid_empty(self): - """ - insures that the is_serial_usb function retunrs false - if the port.vid param is empty - """ - port = ListPortInfo() - port.vid = None - args = Namespace() - args.vid = None - - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_args_vid_not_empty(self): - """ - checks to make sure that the is_serial_usb function - exists correctly when the args["vid"] is not empty - and port["vid"] doesn't equal args["vid"] - """ - port = ListPortInfo() - port.vid = "foo" - args = Namespace() - args.vid = "bar" - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_args_pid_not_empty(self): - """ - checks to make sure that the is_serial_usb function - exists correctly when the args["pid"] is not empty - and port["[id"] doesn't equal args["vid"] - """ - port = ListPortInfo() - args = Namespace() - port.vid = "bar" - port.pid = "foo" - - args.vid = None - args.pid = "bar" - - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_args_vendor_not_empty(self): - """ - checks to make sure that the is_serial_usb function - exists correctly when the args["vendor"] is not empty - and port["manufacturer"] doesn't start with args["vendor"] - """ - port, args = ListPortInfo(), Namespace() - port.vid = "bar" - port.pid = "foo" - port.manufacturer = "Apple" - - args.vid = None - args.pid = None - args.vendor = "Microsoft" - - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_args_serial_not_empty(self): - """ - checks to make sure that the is_serial_usb function - exists correctly when the args["serial"] is not empty - and port["serial_number"] doesn't start with args["serial"] - """ - port, args = ListPortInfo(), Namespace() - - port.vid = "bar" - port.pid = "foo" - port.manufacturer = "Apple" - port.serial_number = "456" - - args.vid = None - args.pid = None - args.vendor = None - args.serial = "123" - - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_args_intf_not_empty(self): - """ - checks to make sure that the is_serial_usb function - exists correctly when the args["serial"] is not empty - and port["interface"] is none - """ - port, args = ListPortInfo(), Namespace() - - port.vid = "bar" - port.pid = "foo" - port.manufacturer = "Apple" - port.serial_number = "456" - port.interface = None - - args.vid = None - args.pid = None - args.vendor = None - args.serial = None - args.intf = "foo" - - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_args_intf_not_empty_interface_not_empty(self): - """ - checks to make sure that the is_serial_usb function - exists correctly when the args["serial"] is not empty - and port["interface"] is different than args["serial"] - """ - port, args = ListPortInfo(), Namespace() - - port.vid = "bar" - port.pid = "foo" - port.manufacturer = "Apple" - port.serial_number = "456" - port.interface = "bar" - - args.vid = None - args.pid = None - args.vendor = None - args.serial = None - args.intf = "foo" - - response = is_usb_serial(port, args) - self.assertFalse(response) - - def test_pass(self): - """ - insure that is_serial_usb returns true if all test cases haven't - failed - """ - port, args = ListPortInfo(), Namespace() - - port.vid = "bar" - port.pid = "foo" - port.manufacturer = "Apple" - port.serial_number = "456" - port.interface = "bar" - - args.vid = None - args.pid = None - args.vendor = None - args.serial = None - args.intf = None - - response = is_usb_serial(port, args) - self.assertTrue(response) - - -class ExtraInfoTests(SimpleTestCase): - def test_manufacturer(self): - """ - insure that the manufacturer is added to the - extra_items list if it is present in port - """ - port = ListPortInfo() - port.manufacturer = "Microsoft" - - response = extra_info(port) - self.assertTrue(port.manufacturer in response) - - def test_no_matches(self): - """ - insure that extra_info returns the empty string if - none of the keys match - """ - port = ListPortInfo() - port.foo = "bar" - - self.assertTrue(extra_info(port) == "") - - def test_serial_number(self): - """ - insure that the serial_number is added to the - extra_items list if it is present in port - """ - port = ListPortInfo() - port.serial_number = "123" - - response = extra_info(port) - self.assertTrue(port.serial_number in response) - - def test_interface(self): - """ - insure that the interface is added to the - extra_items list if it is present in port - """ - port = ListPortInfo() - port.interface = "123interface" - - response = extra_info(port) - self.assertTrue(port.interface in response) - - -class GetPortTests(SimpleTestCase): - @mock.patch("serial.tools.list_ports.comports") - def test_get_port_match(self, port_mocks): - - port = ListPortInfo() - port.vid = "vid" - port.pid = None - port.manufacturer = None - port.serial_number = None - port.interface = None - port.device = "usb" - - port_mocks.return_value = [port] - self.assertTrue("port found" in get_port()) - - @mock.patch("serial.tools.list_ports.comports") - def test_get_port_empty(self, port_mocks): - - port = ListPortInfo() - port.vid = None - port.pid = None - port.manufacturer = None - port.serial_number = None - port.interface = None - port.device = "usb" - - port_mocks.return_value = [port] - self.assertIsNone(get_port()) diff --git a/hardware/tests/test_lan_server.py b/hardware/tests/test_lan_server.py new file mode 100644 index 00000000..4c41d4fd --- /dev/null +++ b/hardware/tests/test_lan_server.py @@ -0,0 +1,170 @@ +from django.test import SimpleTestCase +from http.server import HTTPServer +from testfixtures import LogCapture, TempDirectory + +from unittest import mock + +import threading +import socket +import requests +import os + +from hardware.CommunicationsPi.lan_server import Server, runServer + + +def get_free_port(): + s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) + s.bind(("localhost", 0)) + address, port = s.getsockname() + s.close() + return port + + +class LanServerTests(SimpleTestCase): + def setUp(self): + self.temp_dir = TempDirectory() + self.mock_server_port = get_free_port() + self.mock_server = HTTPServer(("localhost", self.mock_server_port), Server) + + self.mock_server_thread = threading.Thread( + target=self.mock_server.serve_forever + ) + self.mock_server_thread.setDaemon(True) + self.mock_server_thread.start() + + def tearDown(self): + self.temp_dir.cleanup() + + def test_get(self, mock_transceiver=mock.MagicMock()): + with mock.patch.dict( + os.environ, + {"LAN_SERVER_LOG_FILE": "logger.txt", "LOG_DIRECTORY": self.temp_dir.path}, + ): + with LogCapture() as capture: + url = f"http://localhost:{self.mock_server_port}/" + response = requests.get(url) + + self.assertTrue(response.ok) + self.assertTrue(response.headers.get("Content-Type") == "text/html") + + capture.check( + ( + "urllib3.connectionpool", + "DEBUG", + f"Starting new HTTP connection (1): localhost:{self.mock_server_port}", + ), + ( + "LAN_SERVER_LOG_FILE", + "INFO", + "GET request,\n" + "Path: %s\n" + "Headers:\n" + "%s\n" + f"/Host: localhost:{self.mock_server_port}\n" + "User-Agent: python-requests/2.23.0\n" + "Accept-Encoding: gzip, deflate\n" + "Accept: */*\n" + "Connection: keep-alive\n" + "\n", + ), + ( + "urllib3.connectionpool", + "DEBUG", + f'http://localhost:{self.mock_server_port} "GET / HTTP/1.1" 200 None', + ), + ) + + def test_post(self, mock_transceiver=mock.MagicMock()): + with mock.patch.dict( + os.environ, + {"LAN_SERVER_LOG_FILE": "logger.txt", "LOG_DIRECTORY": self.temp_dir.path}, + ): + with LogCapture() as capture: + url = f"http://localhost:{self.mock_server_port}/" + response = requests.post( + url, data={"key": "value"}, headers={"Content-Length": "15"} + ) + + self.assertTrue(response.ok) + self.assertTrue(response.headers.get("Content-Type") == "text/html") + + capture.check( + ( + "urllib3.connectionpool", + "DEBUG", + f"Starting new HTTP connection (1): localhost:{self.mock_server_port}", + ), + ( + "LAN_SERVER_LOG_FILE", + "INFO", + "POST request,\n" + "Path: %s\n" + "Headers:\n" + "%s\n" + "\n" + "Body:\n" + "%s\n" + f"/Host: localhost:{self.mock_server_port}\n" + "User-Agent: python-requests/2.23.0\n" + "Accept-Encoding: gzip, deflate\n" + "Accept: */*\n" + "Connection: keep-alive\n" + "Content-Length: 9\n" + "Content-Type: application/x-www-form-urlencoded\n" + "\n" + "key=value", + ), + ("LAN_SERVER_LOG_FILE", "INFO", "data: b'key=value'"), + ( + "urllib3.connectionpool", + "DEBUG", + f'http://localhost:{self.mock_server_port} "POST / HTTP/1.1" 200 None', + ), + ) + + +class RunServerTests(SimpleTestCase): + def setUp(self): + self.temp_dir = TempDirectory() + + def tearDown(self): + self.temp_dir.cleanup() + + def test_run_server(self): + with mock.patch.dict( + os.environ, + { + "LAN_SERVER_LOG_FILE": "logger.txt", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_PORT": "0000", + }, + ): + with LogCapture() as capture: + mock_server = mock.MagicMock() + mock_server.return_value.server_forever = mock.MagicMock() + mock_handler = mock.MagicMock() + + runServer(mock_server, mock_handler) + + capture.check( + ("LAN_SERVER_LOG_FILE", "INFO", "Starting server on port: 0"), + ("LAN_SERVER_LOG_FILE", "INFO", "Stopping\n"), + ) + + +# def test_interrupt(self): +# with mock.patch.dict(os.environ, { +# "LAN_SERVER_LOG_FILE": "logger.txt", +# "LOG_DIRECTORY": self.temp_dir.path, +# "LAN_PORT": "0000" +# }): +# with LogCapture() as capture: +# mock_server = mock.MagicMock() +# mock_server.return_value.server_forever.side_effect = KeyboardInterrupt +# mock_handler = mock.MagicMock() + +# with self.assertRaises(KeyboardInterrupt): +# runServer(mock_server, mock_handler) + +# capture.check(('LAN_SERVER_LOG_FILE', 'INFO', 'Starting server on port: 0'), +# ('LAN_SERVER_LOG_FILE', 'INFO', 'Stopping\n')) diff --git a/hardware/tests/test_radio_transceiver.py b/hardware/tests/test_radio_transceiver.py index 397f1a12..8df5db04 100644 --- a/hardware/tests/test_radio_transceiver.py +++ b/hardware/tests/test_radio_transceiver.py @@ -6,6 +6,7 @@ from serial.tools.list_ports_common import ListPortInfo import os +import json import serial from hardware.CommunicationsPi.radio_transceiver import Transceiver @@ -21,7 +22,7 @@ def setUp(self): self.parity = serial.PARITY_NONE self.stopbits = serial.STOPBITS_ONE self.bytesize = serial.EIGHTBITS - self.timeout = "1" + self.timeout = 1 def tearDown(self): self.temp_dir.cleanup() @@ -313,7 +314,6 @@ def test_is_serial_usb_vid_no_match(self, mock_port_list, mock_serial): self.assertTrue(transciever.logging is not None) self.assertTrue(transciever.logging.name == "LOG_FILE") self.assertIsInstance(transciever.logging, Logger) - self.assertTrue(transciever.port == "usb2") self.assertTrue(transciever.port_vid == "foo2") self.assertTrue(transciever.port_pid == "baz") @@ -687,3 +687,49 @@ def test_listen_invalid(self, mock_port_list, mock_serial): ("LOG_FILE", "INFO", "Opening serial on: usb"), ("LOG_FILE", "ERROR", ""), ) + + @patch.object(json, "loads") + @patch("serial.Serial") + @patch("serial.tools.list_ports.comports") + def test_listen_exception(self, mock_port_list, mock_serial, mock_json): + """ + tests the listen method with invalid input + """ + port = ListPortInfo() + port.vid = "vid" + port.pid = "pid" + port.manufacturer = "Microsoft" + port.serial_number = "456" + port.interface = "usb" + port.device = "usb" + + mock_json.side_effect = Exception("ex") + + mock_port_list.return_value = [port] + + test_input = "{'value': 'value'}" + with patch.dict( + os.environ, + { + "LOG_DIRECTORY": self.temp_dir.path, + "RADIO_TRANSMITTER_PORT": "usb", + "LOG_FILE": "logger.txt", + "TRANSCEIVER_BAUDRATE": "9600", + "TRANSCEIVER_TIMEOUT": "1", + }, + ): + with LogCapture() as capture: + transceiver = Transceiver(log_file_name="LOG_FILE") + + mock_receiver = MagicMock() + mock_receiver.readline.return_value.decode.return_value = test_input + transceiver.serial = mock_receiver + + with self.assertRaises(Exception): + transceiver.listen() + + capture.check( + ("LOG_FILE", "INFO", "Port device found: usb"), + ("LOG_FILE", "INFO", "Opening serial on: usb"), + ("LOG_FILE", "ERROR", "error occurred: ex"), + ) diff --git a/hardware/tests/test_utils.py b/hardware/tests/test_utils.py index 316a2b85..0be4692d 100644 --- a/hardware/tests/test_utils.py +++ b/hardware/tests/test_utils.py @@ -4,8 +4,14 @@ from testfixtures import TempDirectory from logging import INFO import os - -from hardware.Utils.utils import get_logger, get_serial_stream +from datetime import datetime +import dateutil.parser + +from hardware.Utils.utils import ( + get_logger, + get_serial_stream, + date_str_with_current_timezone, +) from hardware.Utils.logger import Logger @@ -36,3 +42,9 @@ def test_serial_stream(self): stream, b'{"id": 5, "value": {"value_a_name": 15.0, "value_b_name": 26.5, "value_c_name": 13.3}}\n', ) + + def test_date_str_with_current_timezone(self): + s = date_str_with_current_timezone() + date = dateutil.parser.isoparse(s) + self.assertTrue("T" in s) + self.assertAlmostEqual(date.timestamp(), datetime.now().timestamp(), places=1) diff --git a/hardware/tests/test_web_client.py b/hardware/tests/test_web_client.py new file mode 100644 index 00000000..cf564db0 --- /dev/null +++ b/hardware/tests/test_web_client.py @@ -0,0 +1,192 @@ +from django.test import SimpleTestCase +from unittest.mock import patch, MagicMock +from testfixtures import TempDirectory, LogCapture +from requests.exceptions import HTTPError + +import os + +from hardware.CommunicationsPi.web_client import WebClient +from hardware.Utils.logger import Logger + + +class WebClientTests(SimpleTestCase): + def setUp(self): + self.temp_dir = TempDirectory() + + def tearDown(self): + self.temp_dir.cleanup() + + def test_init_no_log_no_server(self): + with patch.dict( + os.environ, + { + "WEB_CLIENT_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + l_client = WebClient() + + self.assertTrue(l_client.logging is not None) + self.assertTrue(l_client.logging.name == "WEB_CLIENT_LOG_FILE") + self.assertIsInstance(l_client.logging, Logger) + + self.assertEqual(l_client.url, "https://0.0.0.0:0") + + def test_init_no_log_no_server_http(self): + with patch.dict( + os.environ, + { + "WEB_CLIENT_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + l_client = WebClient() + + self.assertTrue(l_client.logging is not None) + self.assertTrue(l_client.logging.name == "WEB_CLIENT_LOG_FILE") + self.assertIsInstance(l_client.logging, Logger) + + self.assertEqual(l_client.url, "http://0.0.0.0:0") + + def test_init_no_log_server(self): + with patch.dict( + os.environ, + { + "WEB_CLIENT_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + l_client = WebClient(server_url="/url") + + self.assertTrue(l_client.logging is not None) + self.assertTrue(l_client.logging.name == "WEB_CLIENT_LOG_FILE") + self.assertIsInstance(l_client.logging, Logger) + + self.assertEqual(l_client.url, "/url") + + def test_init_log_no_server(self): + with patch.dict( + os.environ, + { + "NEW_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + l_client = WebClient(log_file_name="NEW_LOG_FILE") + + self.assertTrue(l_client.logging is not None) + self.assertTrue(l_client.logging.name == "NEW_LOG_FILE") + self.assertIsInstance(l_client.logging, Logger) + + self.assertEqual(l_client.url, "https://0.0.0.0:0") + + def test_init_log_server(self): + with patch.dict( + os.environ, + { + "NEW_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + l_client = WebClient(log_file_name="NEW_LOG_FILE", server_url="/url") + + self.assertTrue(l_client.logging is not None) + self.assertTrue(l_client.logging.name == "NEW_LOG_FILE") + self.assertIsInstance(l_client.logging, Logger) + + self.assertEqual(l_client.url, "/url") + + @patch("hardware.CommunicationsPi.web_client.requests") + def test_ping_server(self, mock_requests=MagicMock()): + with patch.dict( + os.environ, + { + "WEB_CLIENT_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + with LogCapture() as capture: + l_client = WebClient() + + payload = "{'key':'value'}" + + l_client.ping_lan_server(payload) + + mock_requests.post.assert_called_with("https://0.0.0.0:0", data=payload) + capture.check( + ("WEB_CLIENT_LOG_FILE", "INFO", "Pinging"), + ("WEB_CLIENT_LOG_FILE", "INFO", f"data: { payload }"), + ) + + @patch("hardware.CommunicationsPi.web_client.requests") + def test_ping_server_raise_http_ex(self, mock_requests=MagicMock()): + with patch.dict( + os.environ, + { + "WEB_CLIENT_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + with LogCapture() as capture: + l_client = WebClient() + mock_requests.post.side_effect = HTTPError("HTTPError") + + payload = "{'key':'value'}" + + with self.assertRaises(HTTPError): + l_client.ping_lan_server(payload) + + mock_requests.post.assert_called_with("https://0.0.0.0:0", data=payload) + capture.check( + ("WEB_CLIENT_LOG_FILE", "INFO", "Pinging"), + ("WEB_CLIENT_LOG_FILE", "INFO", f"data: { payload }"), + ("WEB_CLIENT_LOG_FILE", "ERROR", "HTTP error occurred: HTTPError"), + ) + + @patch("hardware.CommunicationsPi.web_client.requests") + def test_ping_server_raise_ex(self, mock_requests=MagicMock()): + with patch.dict( + os.environ, + { + "WEB_CLIENT_LOG_FILE": "web_client.log", + "LOG_DIRECTORY": self.temp_dir.path, + "LAN_SERVER_HTTPS": "True", + "LAN_SERVER_IP": "0.0.0.0", + "LAN_PORT": "0", + }, + ): + with LogCapture() as capture: + l_client = WebClient() + mock_requests.post.side_effect = Exception("Exception") + + payload = "{'key':'value'}" + + with self.assertRaises(Exception): + l_client.ping_lan_server(payload) + + mock_requests.post.assert_called_with("https://0.0.0.0:0", data=payload) + capture.check( + ("WEB_CLIENT_LOG_FILE", "INFO", "Pinging"), + ("WEB_CLIENT_LOG_FILE", "INFO", f"data: { payload }"), + ("WEB_CLIENT_LOG_FILE", "ERROR", "error occurred: Exception"), + ) diff --git a/requirements.txt b/requirements.txt index 46126f78..33b7cc01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,8 +14,8 @@ drf-yasg selenium==3.141.0 django-annoying mock -requests +requests==2.23.0 matplotlib attr django-bootstrap4 -django-js-reverse \ No newline at end of file +django-js-reverse diff --git a/symmetricds/README.md b/symmetricds/README.md index 0dd03abc..1c79b496 100644 --- a/symmetricds/README.md +++ b/symmetricds/README.md @@ -60,10 +60,11 @@ postgres://:@ ``` db.user= db.password= -db.url=jdbc:postgresql://?sslmode=require +db.url=jdbc:postgresql://?sslmode=require&stringtype=unspecified ``` -Please make sure your db.url starts with `jdbc:postgresql://` and you appended `?sslmode=require` at the end. +Please make sure your db.url starts with `jdbc:postgresql://` and you appended `?sslmode=require&stringtype=unspecified` at the end. +To synchronize json fields, you should set `stringtype=unspecified`. Otherwise postgres assumes `varchar` by default. [Link](https://jdbc.postgresql.org/documentation/head/connect.html) For example, given the following output, ``` @@ -77,7 +78,7 @@ Your `engine1.properties` should be like: ``` db.user=abcdeabcdefghi db.password=11d804e1c01111a9c111114fcc528753829a314c30cc51938f4192979102c12 -db.url=jdbc:postgresql://ec2-1-000-00-000.compute-2.amazonaws.com:5432/948f928kjfjv827?sslmode=require +db.url=jdbc:postgresql://ec2-1-000-00-000.compute-2.amazonaws.com:5432/948f928kjfjv827?sslmode=require&stringtype=unspecified ``` ### Step 3 diff --git a/symmetricds/template/engine0.properties b/symmetricds/template/engine0.properties index 7f20093b..19f01cc5 100644 --- a/symmetricds/template/engine0.properties +++ b/symmetricds/template/engine0.properties @@ -11,8 +11,10 @@ db.user=postgres db.password= # https://jdbc.postgresql.org/documentation/80/connect.html -# db.url=jdbc:postgresql://localhost:5432/mercury -db.url=jdbc:postgresql://localhost:5432/mercury +# db.url=jdbc:postgresql://localhost:5432/mercury?stringtype=unspecified +# +# Set stringtype=unspecified to synchronize json fields. Otherwise postgres assumes varchar by default. +db.url=jdbc:postgresql://localhost:5432/mercury?stringtype=unspecified diff --git a/symmetricds/template/engine1.properties b/symmetricds/template/engine1.properties index a92885d2..b8f57bca 100644 --- a/symmetricds/template/engine1.properties +++ b/symmetricds/template/engine1.properties @@ -13,9 +13,10 @@ registration.url= # 3. Your configuration would be: # db.user= # db.password= -# db.url=jdbc:postgresql://?sslmode=require - -# Please make sure your db.url starts with "jdbc:postgresql://" and you appended "?sslmode=require" at the end. +# db.url=jdbc:postgresql://?sslmode=require&stringtype=unspecified +# +# Please make sure your db.url starts with "jdbc:postgresql://" and you appended "?sslmode=require&stringtype=unspecified" at the end. +# Set stringtype=unspecified to synchronize json fields. Otherwise postgres assumes varchar by default. db.user= db.password=