Skip to content

Commit

Permalink
Transferred and fixed, modernised the code
Browse files Browse the repository at this point in the history
  • Loading branch information
TheCodeSummoner committed Dec 22, 2020
1 parent f7dbc1c commit 94fce59
Show file tree
Hide file tree
Showing 19 changed files with 793 additions and 0 deletions.
1 change: 1 addition & 0 deletions .autostart.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sudo python3.8 -m ncl_rovers &
37 changes: 37 additions & 0 deletions .github/workflows/python-testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Python package testing

on:
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]

steps:
- uses: actions/checkout@v2

- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install the package
run : |
python -m pip install --upgrade pip setuptools wheel
python -m pip install .
- name: Static analysis (pylint + pydocstyle)
run: |
python -m pip install pylint pydocstyle
pylint --rcfile .pylintrc raspberry_pi tests setup.py
pydocstyle --config .pydocstylerc raspberry_pi tests setup.py
- name: Tests (pytest)
run: |
python -m pip install pytest pytest-cov
pytest --cov=raspberry_pi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# JetBrains
.idea
3 changes: 3 additions & 0 deletions .pydocstylerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pydocstyle]
add_ignore = D200, D105, D107
add_select = D213, D214, D215, D404
11 changes: 11 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[FORMAT]
max-line-length=120

[MESSAGES CONTROL]
disable = logging-fstring-interpolation, too-few-public-methods, too-many-instance-attributes

[SIMILARITIES]
min-similarity-lines=4
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes
3 changes: 3 additions & 0 deletions raspberry_pi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Raspberry Pi package.
"""
16 changes: 16 additions & 0 deletions raspberry_pi/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Raspberry Pi execution file.
"""
import os
import sys

# Make sure a local raspberry-pi package can be found and overrides any installed versions
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from raspberry_pi.data_manager import DataManager # pylint: disable = wrong-import-position
from raspberry_pi.server import Server # pylint: disable = wrong-import-position

if __name__ == '__main__':
dm = DataManager()
server = Server(dm)
server.start()
170 changes: 170 additions & 0 deletions raspberry_pi/arduino.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
Representation of an Arduino, including relevant networking functionalities.
"""
from threading import Thread
import msgpack
from msgpack import UnpackException
from serial import Serial, SerialException
from .data_manager import DataManager
from .constants import SERIAL_BAUDRATE, SERIAL_READ_TIMEOUT, SERIAL_WRITE_TIMEOUT
from .enums import Device
from .utils import logger


class Arduino:
"""
Handle serial communication between the ROV and an Arduino.
The Arduino-s are created via server, and can be accessed in the following way:
arduinos = server.arduinos
While working, the code should check if the communication is happening, to detect when it stops:
if not arduino.connected():
arduino.reconnect()
You can check __main__.py to see how the surface-rov and rov-arduino(s) connections are kept alive.
"""

def __init__(self, dm: DataManager, port: str):
"""
Initialise the serial object and the thread.
At first, the device is not identified (it's unassigned), and will be known after the pre-communication process.
"""
self._dm = dm
self._port = port
self._device = Device.UNASSIGNED
self._communication_thread = self._new_thread()
self._serial = Serial(baudrate=SERIAL_BAUDRATE)
self._serial.port = self._port
self._serial.write_timeout = SERIAL_WRITE_TIMEOUT
self._serial.timeout = SERIAL_READ_TIMEOUT

def __str__(self):
return self._port

@property
def connected(self) -> bool:
"""
Check if the communication is happening.
"""
return self._communication_thread.is_alive()

def _new_thread(self) -> Thread:
"""
Create the communication thread.
"""
return Thread(target=self._communicate)

def connect(self):
"""
Connect to the Arduino and start exchanging the data.
Opens a serial connection and starts the communication thread.
"""
if self.connected:
logger.error(f"Can't connect - already connected to {self._port}")
return

logger.info(f"Connecting to {self._port}")
while True:
try:
if not self._serial.is_open:
self._serial.open()
break
except SerialException as ex:
logger.debug(f"Failed to connect to {self._port} - {ex}")

logger.info(f"Connected to {self._port}")
self._communication_thread.start()

def _communicate(self):
"""
Exchange the data with the Arduino.
Pre-communicates at first, to identify the device and set relevant ID, while ignoring empty data. Starts
exchanging information properly immediately after.
"""
while True:
try:
data = self._serial.read_until().strip()
if not data:
continue

try:
self._device = Device(msgpack.unpackb(data.decode("utf-8"))["ID"])
except (UnicodeError, UnpackException):
logger.exception(f"Failed to decode the following data in pre-communication: {data}")
return

# Knowing the id, set the connection status to connected (True) and exit the pre-communication step
logger.info(f"Detected a valid device at {self._port} - {self._device.name}")
self._dm.set(self._device, **{self._device.value: True})
break

except SerialException:
logger.exception(f"Lost connection to {self._port}")
return

except (KeyError, ValueError):
logger.error(f"Invalid device ID received from {self._port}")
return

while True:
try:
if data:
logger.debug(f"Received data from {self._port} - {data}")

try:
data = msgpack.unpackb(data.decode("utf-8").strip())
except (UnicodeError, UnpackException):
logger.exception(f"Failed to decode following data: {data}")

# Remove ID from the data to avoid setting it upstream, disconnect in case of errors
if "ID" not in data or data["ID"] != self._device.value:
logger.error(f"ID key not in {data} or key doesn't match {self._device.value}")
break

del data["ID"]
self._dm.set(self._device, **data)

else:
logger.debug(f"Timed out reading from {self._port}, clearing the buffer")
self._serial.reset_output_buffer()

# Send data and wait for a response from Arduino (next set of data to process)
self._serial.write(bytes(msgpack.packb(self._dm.get(self._device)) + "\n"))
data = self._serial.read_until().strip()

except SerialException:
logger.error(f"Lost connection to {self._port}")
break

def disconnect(self):
"""
Disconnect from the Arduino and stop exchanging the data.
"""
try:
if self._serial.is_open:
self._serial.close()
except SerialException:
logger.exception(f"Failed to safely disconnect from {self._port}")

# Clean up the communication thread
self._communication_thread = self._new_thread()

# Set the connection status to disconnected, if the id was known
if self._device != Device.UNASSIGNED:
self._dm.set(self._device, **{self._device.value: False})

# Forget the device id
self._device = Device.UNASSIGNED

def reconnect(self):
"""
Reconnect to the Arduino.
"""
self.disconnect()
self.connect()
76 changes: 76 additions & 0 deletions raspberry_pi/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""
Constants and other static values.
"""
import os
import dotenv
from .enums import Device

# Declare paths to relevant folders - tests folder shouldn't be known here
ROOT_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
RASPBERRY_PI_DIR = os.path.join(ROOT_DIR, "raspberry_pi")
RES_DIR = os.path.join(RASPBERRY_PI_DIR, "res")
LOG_DIR = os.path.join(RASPBERRY_PI_DIR, "log")

# Load the environment variables from the root folder and/or the resources folder
dotenv.load_dotenv(dotenv_path=os.path.join(ROOT_DIR, ".env"))
dotenv.load_dotenv(dotenv_path=os.path.join(RES_DIR, ".env"))

# Declare logging config
LOG_CONFIG_PATH = os.getenv("LOG_CONFIG_PATH", os.path.join(RES_DIR, "log-config.json"))
LOGGER_NAME = os.getenv("LOGGER_NAME", "raspberry-pi")

# Declare surface connection information
CONNECTION_IP = os.getenv("CONNECTION_IP", "0.0.0.0")
CONNECTION_PORT = int(os.getenv("CONNECTION_PORT", "50000"))
CONNECTION_DATA_SIZE = int(os.getenv("CONNECTION_DATA_SIZE", "4096"))

# Declare Arduino-related constants (timeouts in seconds)
ARDUINO_PORTS = tuple(port for port in os.getenv("ARDUINO_PORTS", "").split(",") if port)
SERIAL_WRITE_TIMEOUT = int(os.getenv("SERIAL_WRITE_TIMEOUT", "1"))
SERIAL_READ_TIMEOUT = int(os.getenv("SERIAL_READ_TIMEOUT", "1"))
SERIAL_BAUDRATE = int(os.getenv("SERIAL_BAUDRATE", "115200"))

# Declare the constant to determine how often should the connection statuses be checked (in seconds)
CONNECTION_CHECK_DELAY = int(os.getenv("CONNECTION_CHECK_DELAY", "1"))

# Declare constant for slowly changing up all values and keys affected
RAMP_RATE = 2
RAMP_KEYS = {
"T_HFP",
"T_HFS",
"T_HAP",
"T_HAS",
"T_VFP",
"T_VFS",
"T_VAP",
"T_VAS",
"T_M"
}


# Declare the transmission sets with the default values as initial values
THRUSTER_IDLE = 1500
GRIPPER_IDLE = 1500
CORD_IDLE = 1500
DEFAULTS = {
Device.SURFACE: {
"A_O": False,
"A_I": False,
"S_O": 0,
"S_I": 0
},
Device.ARDUINO_O: {
"T_HFP": THRUSTER_IDLE,
"T_HFS": THRUSTER_IDLE,
"T_HAP": THRUSTER_IDLE,
"T_HAS": THRUSTER_IDLE,
"T_VFP": THRUSTER_IDLE,
"T_VFS": THRUSTER_IDLE,
"T_VAP": THRUSTER_IDLE,
"T_VAS": THRUSTER_IDLE,
"T_M": THRUSTER_IDLE,
"M_G": GRIPPER_IDLE,
"M_C": CORD_IDLE
},
Device.ARDUINO_I: {}
}
Loading

0 comments on commit 94fce59

Please sign in to comment.