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=