Skip to content

Commit

Permalink
Merge branch 'development' into inverse-tf
Browse files Browse the repository at this point in the history
  • Loading branch information
EirikKolas authored Apr 20, 2024
2 parents 6883fd3 + ab36543 commit be68ad3
Show file tree
Hide file tree
Showing 42 changed files with 1,420 additions and 342 deletions.
1 change: 1 addition & 0 deletions acoustics/acoustics_data_record/acoustics_data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,56 @@


class TeensyCommunicationUDP:
"""
This class is responsible for the RPI side of teensy-RPI UDP communication. It is
implemented with a singleton pattern for convenience.
Note: Private members are denoted by _member_name
Attributes:
-----------
_TEENSY_IP (string): self-explanatory
_TEENSY_PORT (int): teensy's data port
_MY_PORT (int): the device's data port
_MAX_PACKAGE_SIZE_RECEIVED (int): max size (in bytes) of UDP receive buffer
_TIMEOUT (int): socket timeout when reading data
_clientSocket (socket): UDP socket for teensy communication
_timeoutMax (int): time to wait before retrying handshake
_data_string (str): buffer for received teensy data
_data_target (str): the field of `acoustics_data` that is written to
acoustics_data (dict[str, list[int]]): containter for data from teensy
Methods:
--------
init_communication(frequenciesOfInterest: list[tuple[int, int]]) -> None:
Sets up socket for communication with teensy and waits for handshake
fetch_data() -> None:
Reads a full data message from teensy and saves it
_write_to_target() -> None:
Writes _data_string to the correct field of acoustics_data
_parse_data_string(is_float: bool) -> list[float] | list[int] | None:
Converts _data_string into a list of either floats or integers
_get_ip() -> None:
Gets the IP address of the device
_send_acknowledge_signal() -> None:
Sends the _INITIALIZATION_MESSAGE to teensy
_check_if_available() -> None:
Checks the UDP message buffer for a READY message
_send_frequencies_of_interest(frequenciesOfInterest: list[tuple[float, float]]) -> None:
Sends the list of frequencies and variances to teensy
"""
# Teensy networking Setup
TEENSY_IP = "10.0.0.111"
TEENSY_PORT = 8888
MY_PORT = 9999
MAX_PACKAGE_SIZE_RECEIVED = 65536
TIMEOUT = 1
address = (TEENSY_IP, TEENSY_PORT)
_TEENSY_IP = "10.0.0.111"
_TEENSY_PORT = 8888
_MY_PORT = 9999
_MAX_PACKAGE_SIZE_RECEIVED = 65536
_TIMEOUT = 1
_address = (_TEENSY_IP, _TEENSY_PORT)

# Code words for communication
INITIALIZATION_MESSAGE = "HELLO :D" # This is a message only sent once to establish 2 way communication between Teensy and client
_INITIALIZATION_MESSAGE = "HELLO :D" # This is a message only sent once to establish 2 way communication between Teensy and client

clientSocket = socket(AF_INET, SOCK_DGRAM)
_clientSocket = socket(AF_INET, SOCK_DGRAM)

_timeoutMax = 10
_data_string = ""
Expand All @@ -39,56 +77,58 @@ class TeensyCommunicationUDP:
}

@classmethod
def init_communication(cls, frequenciesOfInterest):
def init_communication(cls, frequenciesOfInterest: list[tuple[int, int]]) -> None:
"""
Sets up communication with teensy
Parameters:
frequenciesOfInterest (list[tuple[int, int]]): List of frequencies to look for
"""
assert len(frequenciesOfInterest) == 10, "Frequency list has to have exactly 10 entries"

_frequenciesOfInterest = frequenciesOfInterest

cls.MY_IP = cls.get_ip()
cls.MY_IP = cls._get_ip()

# Socket setup
cls.clientSocket.settimeout(cls.TIMEOUT)
cls.clientSocket.bind((cls.MY_IP, cls.MY_PORT))
cls.clientSocket.setblocking(False)
cls._clientSocket.settimeout(cls._TIMEOUT)
cls._clientSocket.bind((cls.MY_IP, cls._MY_PORT))
cls._clientSocket.setblocking(False)

cls.send_acknowledge_signal()
cls._send_acknowledge_signal()
timeStart = time.time()

# Wait for READY signal
while not cls.check_if_available():
"""
IMPORTANT!
DO NOT have "time.sleep(x)" value SMALLER than 1 second!!!
This will interrupt sampling by asking teensy if its available to many times
If less than 1 second you risc crashing teensy to PC communication O_O
"""

while not cls._check_if_available():
print("Did not receive READY signal. Will wait.")
time.sleep(1)

if time.time() - timeStart > cls._timeoutMax:
print("Gave up on receiving READY. Sending acknowledge signal again")
# Start over
timeStart = time.time()
cls.send_acknowledge_signal()
cls._send_acknowledge_signal()

print("READY signal received, sending frequencies...")
cls.send_frequencies_of_interest(frequenciesOfInterest)
cls._send_frequencies_of_interest(frequenciesOfInterest)

@classmethod
def fetch_data(cls):
def fetch_data(cls) -> None:
"""
Gets data from teensy and stores it in `acoustics_data`
"""
i = 0

while True:
data = cls.get_raw_data()
data = cls._get_raw_data()

if data == None:
return

if data not in cls.acoustics_data.keys():
cls._data_string += data
else:
cls.write_to_target()
cls._write_to_target()
cls._data_target = data

# Ah yes, my good friend code safety
Expand All @@ -100,11 +140,14 @@ def fetch_data(cls):
break

@classmethod
def write_to_target(cls):
def _write_to_target(cls) -> None:
"""
Writes to the current target in `acoustics_data` and clears the data string
"""
if cls._data_target == "TDOA" or cls._data_target == "LOCATION":
data = cls.parse_data_string(is_float=True)
data = cls._parse_data_string(is_float=True)
else:
data = cls.parse_data_string(is_float=False)
data = cls._parse_data_string(is_float=False)

if data == None:
cls._data_string = ""
Expand All @@ -115,9 +158,15 @@ def write_to_target(cls):
cls._data_string = ""

@classmethod
def get_raw_data(cls) -> str | None:
def _get_raw_data(cls) -> str | None:
"""
Gets a message from teensy
Returns:
The message in the UDP buffer if there is one
"""
try:
rec_data, _ = cls.clientSocket.recvfrom(cls.MAX_PACKAGE_SIZE_RECEIVED)
rec_data, _ = cls._clientSocket.recvfrom(cls._MAX_PACKAGE_SIZE_RECEIVED)
messageReceived = rec_data.decode()
return messageReceived
except error as e: # `error` is really `socket.error`
Expand All @@ -127,7 +176,16 @@ def get_raw_data(cls) -> str | None:
print("Socket error: ", e)

@classmethod
def parse_data_string(cls, is_float: bool):
def _parse_data_string(cls, is_float: bool) -> list[float] | list[int] | None:
"""
Converts _data_string to a list
Parameters:
is_float (bool): whether _data_string should be seen as a list of floats or ints
Returns:
The converted list
"""
if cls._data_string == '': return

try:
Expand All @@ -143,12 +201,15 @@ def parse_data_string(cls, is_float: bool):
# stackoverflow <3
# https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib
@classmethod
def get_ip(cls):
def _get_ip(cls) -> None:
"""
Gets the device's IP address
"""
s = socket(AF_INET, SOCK_DGRAM)
s.settimeout(0)
try:
# doesn't even have to be reachable
s.connect((cls.TEENSY_IP, 1))
s.connect((cls._TEENSY_IP, 1))
IP = s.getsockname()[0]
except Exception:
IP = '127.0.0.1'
Expand All @@ -158,22 +219,31 @@ def get_ip(cls):
return IP

@classmethod
def send_acknowledge_signal(cls):
def _send_acknowledge_signal(cls) -> None:
"""
Sends "HELLO :D to teensy
"""
try:
cls.clientSocket.sendto(cls.INITIALIZATION_MESSAGE.encode(), cls.address)
cls._clientSocket.sendto(cls._INITIALIZATION_MESSAGE.encode(), cls._address)
print("DEBUGING: Sent acknowledge package")
except Exception as e:
print("Error from send_acknowledge_signal")
print(e)
pass

@classmethod
def check_if_available(cls):
def _check_if_available(cls) -> None:
"""
Checks if READY has been received
Note: The while loop here may not be necessary, it is just there to make absolutely sure that *all*
the data in the UDP buffer is read out when waiting for ready signal, to avoid strange bugs
"""
try:
i = 0
while True:
# Read data
message = cls.get_raw_data()
message = cls._get_raw_data()
# Check if there is no more data left
if message == None:
return False
Expand All @@ -193,9 +263,12 @@ def check_if_available(cls):
return False

@classmethod
def send_frequencies_of_interest(cls, frequenciesOfInterest: list[tuple[float, float]]):
def _send_frequencies_of_interest(cls, frequenciesOfInterest: list[tuple[float, float]]) -> None:
"""
To ignore entries in the frequency list, just make the frequency entry -1, and make the variance 0
Sends the list of frequencies with variance to teensy
Parameters:
frequenciesOfInterest (list[tuple[float, float]]): The list of frequencies w/ variance
"""
try:

Expand All @@ -207,7 +280,7 @@ def send_frequencies_of_interest(cls, frequenciesOfInterest: list[tuple[float, f
frequency_variance_msg = f"{str(frequency)},{str(variance)},"

# print(self.address);
cls.clientSocket.sendto(frequency_variance_msg.encode(), cls.address)
cls._clientSocket.sendto(frequency_variance_msg.encode(), cls._address)
except:
print("Couldn't send Frequency data")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@
from acoustics_interface.acoustics_interface_lib import TeensyCommunicationUDP

class AcousticsInterfaceNode(Node):
"""
Publishes Acoustics data to ROS2
Methods:
data_update() -> None:
calls fetch_data() from acoustics_interface
data_publisher(self) -> None:
publishes data to ROS2 topics
"""
def __init__(self) -> None:
"""
Sets up acoustics logging and publishers, also sets up teensy communication
"""
super().__init__('acoustics_interface')
rclpy.logging.initialize()

Expand Down Expand Up @@ -52,10 +64,11 @@ def __init__(self) -> None:
self.get_logger().info("Sucsefully connected to Acoustics PCB MCU :D")


def data_update(self):
def data_update(self) -> None:
TeensyCommunicationUDP.fetch_data()

def data_publisher(self):
def data_publisher(self) -> None:
"""Publishes to topics"""
self._hydrophone_1_publisher.publish(Int32MultiArray(data=TeensyCommunicationUDP.acoustics_data["HYDROPHONE_1"]))
self._hydrophone_2_publisher.publish(Int32MultiArray(data=TeensyCommunicationUDP.acoustics_data["HYDROPHONE_2"]))
self._hydrophone_3_publisher.publish(Int32MultiArray(data=TeensyCommunicationUDP.acoustics_data["HYDROPHONE_3"]))
Expand Down
8 changes: 4 additions & 4 deletions asv_setup/config/robots/freya.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,18 @@
yaw: true
thrusters:
num: 4
min: -100
max: 100
min: -200
max: 200
configuration_matrix: #NED
[0.70711, 0.70711, 0.70711, 0.70711,
-0.70711, 0.70711, -0.70711, 0.70711,
0.27738, 0.27738, -0.27738, -0.27738]
-0.8485, -0.8485, 0.8485, 0.8485]

rate_of_change:
max: 0 # Maximum rate of change in newton per second for a thruster

thruster_to_pin_mapping: [3, 2, 1, 0] # I.e. if thruster_to_pin = [1, 3, 2, 0] then thruster 0 is pin 1 etc..
thruster_direction: [1,-1,-1, 1] # Disclose during thruster mapping (+/- 1)
thruster_direction: [-1, 1, 1,-1] # Disclose during thruster mapping (+/- 1)
thruster_PWM_offset: [0, 0, 0, 0] # Disclose during thruster mapping, Recomended: [0, 0, 0, 0]
thruster_PWM_min: [1100, 1100, 1100, 1100] # Minimum PWM value, Recomended: [1100, 1100, 1100, 1100]
thruster_PWM_max: [1900, 1900, 1900, 1900] # Maximum PWM value, Recomended: [1900, 1900, 1900, 1900]
Expand Down
35 changes: 35 additions & 0 deletions guidance/d_star_lite/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
cmake_minimum_required(VERSION 3.8)
project(d_star_lite)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# find dependencies
find_package(ament_cmake_python REQUIRED)
find_package(rclpy REQUIRED)
find_package(std_msgs REQUIRED)
find_package(vortex_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)

ament_python_install_package(${PROJECT_NAME})

install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}
)

install(PROGRAMS
d_star_lite/ros_d_star_lite_node.py
DESTINATION lib/${PROJECT_NAME}
)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
find_package(ament_cmake_pytest REQUIRED)
ament_add_pytest_test(python_tests tests)
set(ament_cmake_copyright_FOUND TRUE)
set(ament_cmake_cpplint_FOUND TRUE)
endif()

ament_package()
24 changes: 24 additions & 0 deletions guidance/d_star_lite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# D* lite
Given a map of obstacles and a minimum safe distance to the obstacles, this algorithm will compute the shortest path from the start point to the end point. This is an example of an obstacle map:

![Obstacle Map](https://drive.google.com/uc?export=download&id=1MohnPBQMoQHHbLaDkbUe43MjBPp5sTiF)

And when using this map when running the D* lite algorithm with safe distance 4.5 we get this path:

![Path](https://drive.google.com/uc?export=download&id=1il-i2aJM3pkQacTO8Jg77_2zDVcZF1ty)

A D* lite object consists of these parameters:

```
dsl_object = DStarLite(obstacle_x, obstacle_y, start, goal, safe_dist_to_obstacle, origin, height, width)
```

where `obstacle_x` and `obstacle_y` are lists containg the x and y coordinates of the obstacles. `start` and `goal` are the start and goal node for the selected mission. `origin`, `height` and `width` are parameters to create the world boundaries and are used to compute `x_min`, `x_max`, `y_min` and `y_max`. See the figures below for visual representation.

![World Grid](https://drive.google.com/uc?export=download&id=1RYXcRTjFWMFRhYBMRx5ILmheNvEKahrY)

![Obstacle](https://drive.google.com/uc?export=download&id=1M43ohD3wpKwmgkjJ44Ut1uOT3f5MlSQI)

# D* lite ROS2 node
The node is responsible for gathering the waypoints found from the algorithm and send them to the waypoint manager. The node receives the mission parameters from the mission planner. It is both a client and a server.

Empty file.
Loading

0 comments on commit be68ad3

Please sign in to comment.