Skip to content

Commit

Permalink
Merge branch 'main' into add_decision_handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Trotyl15 authored Sep 25, 2024
2 parents 451e03d + 5c3894e commit 5689e41
Show file tree
Hide file tree
Showing 51 changed files with 1,959 additions and 63 deletions.
26 changes: 16 additions & 10 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This workflow will install Python dependencies and run tests with PyTest using Python 3.8
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
# This workflow will install Python dependencies and run tests with PyTest using Python 3.11
# For more information see: https://docs.github.com/en/actions/about-github-actions

name: Run tests

Expand All @@ -16,23 +16,29 @@ jobs:
steps:
# Checkout repository
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive

# Set Python version
- name: Set up Python 3.8
uses: actions/setup-python@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.11

# Upgrade pip
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
# Set up submodules
- name: Set up all submodules and project dependencies
run: |
git submodule update --init --recursive --remote
git submodule foreach --recursive "pip install -r requirements.txt"
# Install project dependencies
- name: Install project dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# Install zbar library to resolve pyzbar import error
Expand All @@ -46,6 +52,6 @@ jobs:
flake8 .
pylint .
# Install dependencies and run tests with PyTest
- name: Run PyTest
# Run unit tests with PyTest
- name: Run unit tests
run: pytest -vv
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "mavlink/dronekit"]
path = mavlink/dronekit
url = https://github.com/UWARG/dronekit.git
branch = WARG-minimal
Empty file added __init__.py
Empty file.
6 changes: 4 additions & 2 deletions camera/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import cv2

from camera.modules.camera_device import CameraDevice
from .modules.camera_device import CameraDevice


IMAGE_LOG_PREFIX = pathlib.Path("logs", "log_image")
Expand All @@ -16,7 +16,9 @@ def main() -> int:
"""
Main function.
"""
device = CameraDevice(0, 100, IMAGE_LOG_PREFIX)
device = CameraDevice(0, 100, str(IMAGE_LOG_PREFIX))

IMAGE_LOG_PREFIX.parent.mkdir(parents=True, exist_ok=True)

while True:
result, image = device.get_image()
Expand Down
4 changes: 2 additions & 2 deletions camera_qr_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

import cv2

from camera.modules import camera_device
from qr.modules import qr_scanner
from .camera.modules import camera_device
from .qr.modules import qr_scanner


def main() -> int:
Expand Down
Empty file added image_encoding/__init__.py
Empty file.
Empty file.
6 changes: 2 additions & 4 deletions image_encoding/modules/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
Decodes images from JPEG bytes to numpy array.
"""

# Used in type annotation of flight interface output
# pylint: disable-next=unused-import
import io

from PIL import Image
import numpy as np


def decode(data: "io.BytesIO | bytes") -> np.ndarray:
def decode(data: "bytes") -> np.ndarray:
"""
Decodes a JPEG encoded image and returns it as a numpy array.
Expand All @@ -19,6 +17,6 @@ def decode(data: "io.BytesIO | bytes") -> np.ndarray:
Returns:
NDArray with in RGB format. Shape is (Height, Width, 3)
"""
image = Image.open(data, formats=["JPEG"])
image = Image.open(io.BytesIO(data), formats=["JPEG"])

return np.asarray(image)
4 changes: 2 additions & 2 deletions image_encoding/modules/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
QUALITY = 80 # Quality of JPEG encoding to use (0-100)


def encode(image_array: np.ndarray) -> "io.BytesIO | bytes":
def encode(image_array: np.ndarray) -> "bytes":
"""
Encodes an image in numpy array form into bytes of a JPEG.
Expand All @@ -25,4 +25,4 @@ def encode(image_array: np.ndarray) -> "io.BytesIO | bytes":
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=QUALITY)

return buffer
return buffer.getvalue()
21 changes: 4 additions & 17 deletions image_encoding/test_image_encode_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@
from PIL import Image
import numpy as np

from image_encoding.modules import decoder
from image_encoding.modules import encoder
from .modules import decoder
from .modules import encoder


ROOT_DIR = "image_encoding"
TEST_IMG = "test.png"
RESULT_IMG = "result.jpg"


def main() -> int:
def test_image_encode_decode() -> int:
"""
Main testing sequence of encoding and decoding an image.
Note that JPEG is a lossy compression algorithm, so data cannot be recovered.
"""
# Get test image in numpy form
im = Image.open(pathlib.Path(ROOT_DIR, TEST_IMG))
Expand All @@ -36,17 +37,3 @@ def main() -> int:

# Check output shape
assert img_array.shape == raw_data.shape

# Note: the following fail since JPEG encoding is lossy
# assert (raw_data == img_array).all()

return 0


if __name__ == "__main__":
result_main = main()

if result_main < 0:
print(f"ERROR: Status code: {result_main}")

print("Done!")
4 changes: 2 additions & 2 deletions kml/test_ground_locations_to_kml.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import pytest

from kml.modules import ground_locations_to_kml
from kml.modules import location_ground
from .modules import ground_locations_to_kml
from .modules import location_ground


PARENT_DIRECTORY = "kml"
Expand Down
Empty file added logger/__init__.py
Empty file.
Empty file added logger/modules/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions logger/modules/config_logger.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Displaying datetime: https://docs.python.org/3/howto/logging.html#displaying-the-date-time-in-messages
# Changing the format of log messages https://docs.python.org/3/howto/logging.html#changing-the-format-of-displayed-messages
logger:
directory_path: "logs"
file_datetime_format: "%Y-%m-%d_%H-%M-%S"
format: "%(asctime)s: [%(levelname)s] %(message)s"
log_datetime_format: "%I:%M:%S"
174 changes: 174 additions & 0 deletions logger/modules/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
Logs debug messages.
"""

import datetime
import inspect
import logging
import os
import pathlib
import sys

# Used in type annotation of logger parameters
# pylint: disable-next=unused-import
import types

from ..read_yaml.modules import read_yaml


CONFIG_FILE_PATH = pathlib.Path(os.path.dirname(__file__), "config_logger.yaml")


class Logger:
"""
Instantiates Logger objects.
"""

__create_key = object()

@classmethod
def create(cls, name: str, enable_log_to_file: bool) -> "tuple[bool, Logger | None]":
"""
Create and configure a logger.
"""
# Configuration settings
result, config = read_yaml.open_config(CONFIG_FILE_PATH)
if not result:
print("ERROR: Failed to load configuration file")
return False, None

# Get Pylance to stop complaining
assert config is not None

try:
log_directory_path = config["logger"]["directory_path"]
file_datetime_format = config["logger"]["file_datetime_format"]
logger_format = config["logger"]["format"]
logger_datetime_format = config["logger"]["log_datetime_format"]
except KeyError as exception:
print(f"Config key(s) not found: {exception}")
return False, None

# Create a unique logger instance
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)

formatter = logging.Formatter(
fmt=logger_format,
datefmt=logger_datetime_format,
)

# Handles logging to terminal
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)

# Handles logging to file
if enable_log_to_file:
# Get the path to the logs directory.
entries = os.listdir(log_directory_path)

if len(entries) == 0:
print("ERROR: The directory for this log session was not found.")
return False, None

log_names = [
entry for entry in entries if os.path.isdir(os.path.join(log_directory_path, entry))
]

# Find the log directory for the current run, which is the most recent timestamp.
log_path = max(
log_names,
key=lambda datetime_string: datetime.datetime.strptime(
datetime_string, file_datetime_format
),
)

filepath = pathlib.Path(log_directory_path, log_path, f"{name}.log")
try:
file = os.open(filepath, os.O_RDWR | os.O_EXCL | os.O_CREAT)
os.close(file)
except OSError:
print("ERROR: Log file already exists.")
return False, None

file_handler = logging.FileHandler(filename=filepath, mode="w")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

return True, Logger(cls.__create_key, logger)

def __init__(self, class_create_private_key: object, logger: logging.Logger) -> None:
"""
Private constructor, use create() method.
"""
assert class_create_private_key is Logger.__create_key, "Use create() method."

self.logger = logger

@staticmethod
def message_and_metadata(message: str, frame: "types.FrameType | None") -> str:
"""
Extracts metadata from frame and appends it to the message.
"""
if frame is None:
return message

# Get Pylance to stop complaining
assert frame is not None

function_name = frame.f_code.co_name
filename = frame.f_code.co_filename
line_number = inspect.getframeinfo(frame).lineno

return f"[{filename} | {function_name} | {line_number}] {message}"

def debug(self, message: str, log_with_frame_info: bool = True) -> None:
"""
Logs a debug level message.
"""
if log_with_frame_info:
logger_frame = inspect.currentframe()
caller_frame = logger_frame.f_back
message = self.message_and_metadata(message, caller_frame)
self.logger.debug(message)

def info(self, message: str, log_with_frame_info: bool = True) -> None:
"""
Logs an info level message.
"""
if log_with_frame_info:
logger_frame = inspect.currentframe()
caller_frame = logger_frame.f_back
message = self.message_and_metadata(message, caller_frame)
self.logger.info(message)

def warning(self, message: str, log_with_frame_info: bool = True) -> None:
"""
Logs a warning level message.
"""
if log_with_frame_info:
logger_frame = inspect.currentframe()
caller_frame = logger_frame.f_back
message = self.message_and_metadata(message, caller_frame)
self.logger.warning(message)

def error(self, message: str, log_with_frame_info: bool = True) -> None:
"""
Logs an error level message.
"""
if log_with_frame_info:
logger_frame = inspect.currentframe()
caller_frame = logger_frame.f_back
message = self.message_and_metadata(message, caller_frame)
self.logger.error(message)

def critical(self, message: str, log_with_frame_info: bool = True) -> None:
"""
Logs a critical level message.
"""
if log_with_frame_info:
logger_frame = inspect.currentframe()
caller_frame = logger_frame.f_back
message = self.message_and_metadata(message, caller_frame)
self.logger.critical(message)
Loading

0 comments on commit 5689e41

Please sign in to comment.