diff --git a/pyproject.toml b/pyproject.toml index 21d1d16e..ec87d890 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ 'pytest-qt', 'pytest-xvfb', 'TCA9555@git+https://github.com/InvincibleRMC/TCA9555', + 'bluerobotics-tsys01@git+https://github.com/bluerobotics/tsys01-python', 'pymavlink', 'mavproxy' ] diff --git a/src/pi/pi_main/launch/pi_launch.py b/src/pi/pi_main/launch/pi_launch.py index 318fd9a4..c0d92c95 100644 --- a/src/pi/pi_main/launch/pi_launch.py +++ b/src/pi/pi_main/launch/pi_launch.py @@ -64,9 +64,9 @@ def generate_launch_description() -> LaunchDescription: ]) ) + # Flood detection flood_sensors_path = get_package_share_directory('flood_detection') - # Launches Realsense flood_detection_launch = IncludeLaunchDescription( PythonLaunchDescriptionSource([ os.path.join( @@ -75,6 +75,17 @@ def generate_launch_description() -> LaunchDescription: ]) ) + # Temperature sensor + temp_sensor_path = get_package_share_directory('temp_sensor') + + temp_sensor_launch = IncludeLaunchDescription( + PythonLaunchDescriptionSource([ + os.path.join( + temp_sensor_path, 'launch', 'temp_sensor_launch.py' + ) + ]) + ) + namespace_launch = GroupAction( actions=[ PushRosNamespace(NAMESPACE), @@ -82,6 +93,7 @@ def generate_launch_description() -> LaunchDescription: pixhawk_launch, # cam_launch, flood_detection_launch, + temp_sensor_launch, pi_info_launch ] ) diff --git a/src/pi/temp_sensor/README.md b/src/pi/temp_sensor/README.md new file mode 100644 index 00000000..e3d903d7 --- /dev/null +++ b/src/pi/temp_sensor/README.md @@ -0,0 +1,37 @@ +# Flood Detection + +## Overview + +This package reads from the temperature sensor. + +## Installation + + +## Usage + +```bash +ros2 launch temp_sensor temp_sensor_launch.py +``` + +## Launch files + +* **temp_sensor_launch.py:** Primary launch file for temp sensor. + +## Nodes + +### temp_sensor + +Reads and publishes temperature. + +#### Published Topics + +* **`/temperature`** ([rov_msgs/temperature]) + + Temperature readings as floats + + +### test + +Tests flooding. + +[rov_msgs/temperature]: ../../rov_msgs/msg/Temperature.msg diff --git a/src/pi/temp_sensor/launch/temp_sensor_launch.py b/src/pi/temp_sensor/launch/temp_sensor_launch.py new file mode 100644 index 00000000..2cf40b50 --- /dev/null +++ b/src/pi/temp_sensor/launch/temp_sensor_launch.py @@ -0,0 +1,25 @@ +from launch.launch_description import LaunchDescription +from launch_ros.actions import Node + + +def generate_launch_description() -> LaunchDescription: + """ + Generate LaunchDescription for temp_sensor. + + Returns + ------- + LaunchDescription + Launches temperature sensor node + + """ + temp_sensor = Node( + package='temp_sensor', + executable='temp_sensor', + emulate_tty=True, + output='screen', + remappings=[('/pi/temperature', '/tether/temperature')] + ) + + return LaunchDescription([ + temp_sensor + ]) diff --git a/src/pi/temp_sensor/package.xml b/src/pi/temp_sensor/package.xml new file mode 100644 index 00000000..7e14ae6d --- /dev/null +++ b/src/pi/temp_sensor/package.xml @@ -0,0 +1,21 @@ + + + + temp_sensor + 0.0.3 + Measures temperature + Benjamin Poulin + Apache License 2.0 + + ros2launch + python3-smbus + + ament_flake8 + ament_pep257 + ament_mypy + python3-pytest + + + ament_python + + diff --git a/src/pi/temp_sensor/resource/temp_sensor b/src/pi/temp_sensor/resource/temp_sensor new file mode 100644 index 00000000..e69de29b diff --git a/src/pi/temp_sensor/setup.cfg b/src/pi/temp_sensor/setup.cfg new file mode 100644 index 00000000..d7800a8f --- /dev/null +++ b/src/pi/temp_sensor/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/temp_sensor +[install] +install_scripts=$base/lib/temp_sensor diff --git a/src/pi/temp_sensor/setup.py b/src/pi/temp_sensor/setup.py new file mode 100644 index 00000000..fcc08416 --- /dev/null +++ b/src/pi/temp_sensor/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup +from glob import glob +import os + +package_name = 'temp_sensor' + +setup( + name=package_name, + version='0.0.3', + packages=[package_name], + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + # Include all launch files. + (os.path.join('share', package_name, 'launch'), + glob('launch/*launch.[pxy][yma]*')) + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Benjamin Poulin', + maintainer_email='bwp18@case.edu', + description='Temperature sensor', + license='Apache License 2.0', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'temp_sensor = temp_sensor.temp_sensor:main', + 'dry_run = temp_sensor.temp_sensor_dry_run:main' + ], + }, +) diff --git a/src/pi/temp_sensor/temp_sensor/__init__.py b/src/pi/temp_sensor/temp_sensor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pi/temp_sensor/temp_sensor/temp_sensor.py b/src/pi/temp_sensor/temp_sensor/temp_sensor.py new file mode 100644 index 00000000..37c412c3 --- /dev/null +++ b/src/pi/temp_sensor/temp_sensor/temp_sensor.py @@ -0,0 +1,42 @@ +import rclpy +from rclpy.node import Node +from rclpy.qos import QoSPresetProfiles +import tsys01 + +from rov_msgs.msg import Temperature + +READING_TIMER_PERIOD = 0.5 # Seconds + + +class TempSensor(Node): + + def __init__(self) -> None: + super().__init__('temp_sensor', parameter_overrides=[]) + self.publisher = self.create_publisher(Temperature, 'temperature', + QoSPresetProfiles.DEFAULT.value) + self.sensor = tsys01.TSYS01() + self.sensor.init() + + self.timer = self.create_timer(READING_TIMER_PERIOD, self.timer_callback) + + def timer_callback(self) -> None: + try: + self.sensor.read() + temp_reading = self.sensor.temperature() + + # If any of the sensors detect water, send true to /tether/flooding + self.publisher.publish(Temperature(reading=temp_reading)) + except OSError: + print('Failed to read temperature, skipping this read') + + +def main(args: None = None) -> None: + rclpy.init() + temp_sensor = TempSensor() + rclpy.spin(temp_sensor) + temp_sensor.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/src/pi/temp_sensor/temp_sensor/temp_sensor_dry_run.py b/src/pi/temp_sensor/temp_sensor/temp_sensor_dry_run.py new file mode 100644 index 00000000..598a05cf --- /dev/null +++ b/src/pi/temp_sensor/temp_sensor/temp_sensor_dry_run.py @@ -0,0 +1,26 @@ +from time import sleep + +import tsys01 # https://github.com/bluerobotics/tsys01-python + + +def main() -> None: + sensor = tsys01.TSYS01() # Use default I2C bus 1 + + sensor.init() + + while True: + try: + sensor.read() # Sometimes throws OSError: [Errno 121] Remote I/O error + print( + sensor.temperature(), # Get temperature in default units (Centigrade) + '\t', + sensor.temperature(tsys01.UNITS_Farenheit) + ) + except OSError: + print('Failed to read temperature, trying again') + + sleep(1) + + +if __name__ == "__main__": + main() diff --git a/src/pi/temp_sensor/test/test_flake8.py b/src/pi/temp_sensor/test/test_flake8.py new file mode 100644 index 00000000..eac16eef --- /dev/null +++ b/src/pi/temp_sensor/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8() -> None: + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/src/pi/temp_sensor/test/test_mypy.py b/src/pi/temp_sensor/test/test_mypy.py new file mode 100644 index 00000000..c2e823a6 --- /dev/null +++ b/src/pi/temp_sensor/test/test_mypy.py @@ -0,0 +1,14 @@ +"""Test mypy on this module.""" + +import os +import pytest +from ament_mypy.main import main + + +@pytest.mark.mypy +@pytest.mark.linter +def test_mypy() -> None: + """Tests mypy on this module.""" + path = os.path.join(os.getcwd(), "..", "..", "..", "pyproject.toml") + error_code = main(argv=["--config", path]) + assert error_code == 0, 'Found code style errors / warnings' diff --git a/src/pi/temp_sensor/test/test_pep257.py b/src/pi/temp_sensor/test/test_pep257.py new file mode 100644 index 00000000..b6808e1d --- /dev/null +++ b/src/pi/temp_sensor/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257() -> None: + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' diff --git a/src/rov_msgs/CMakeLists.txt b/src/rov_msgs/CMakeLists.txt index 786534e4..422f3034 100644 --- a/src/rov_msgs/CMakeLists.txt +++ b/src/rov_msgs/CMakeLists.txt @@ -29,6 +29,7 @@ rosidl_generate_interfaces(${PROJECT_NAME} "msg/Manip.msg" "msg/CameraControllerSwitch.msg" "msg/Flooding.msg" + "msg/Temperature.msg" "msg/IPAddress.msg" "msg/VehicleState.msg" "msg/ValveManip.msg" diff --git a/src/rov_msgs/msg/Temperature.msg b/src/rov_msgs/msg/Temperature.msg new file mode 100644 index 00000000..72f08d4c --- /dev/null +++ b/src/rov_msgs/msg/Temperature.msg @@ -0,0 +1 @@ +float64 reading \ No newline at end of file diff --git a/src/surface/gui/gui/operator_app.py b/src/surface/gui/gui/operator_app.py index 7309acfd..c1abc504 100644 --- a/src/surface/gui/gui/operator_app.py +++ b/src/surface/gui/gui/operator_app.py @@ -5,6 +5,10 @@ from gui.widgets.timer import InteractiveTimer from gui.widgets.task_selector import TaskSelector from gui.widgets.flood_warning import FloodWarning +from gui.widgets.temperature import TemperatureSensor +from gui.widgets.heartbeat import HeartbeatWidget +from gui.widgets.ip_widget import IPWidget +from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QTabWidget, QWidget, QVBoxLayout, QHBoxLayout @@ -37,6 +41,9 @@ def __init__(self) -> None: flood_warning = FloodWarning() right_pane.addWidget(flood_warning) + temp_sensor = TemperatureSensor() + right_pane.addWidget(temp_sensor) + right_pane.addStretch() task_selector = TaskSelector() diff --git a/src/surface/gui/gui/widgets/temperature.py b/src/surface/gui/gui/widgets/temperature.py new file mode 100644 index 00000000..dcf6a80e --- /dev/null +++ b/src/surface/gui/gui/widgets/temperature.py @@ -0,0 +1,68 @@ +from collections import deque + +from gui.gui_nodes.event_nodes.subscriber import GUIEventSubscriber +from rov_msgs.msg import Temperature +from PyQt6.QtCore import pyqtSignal, pyqtSlot +from PyQt6.QtWidgets import (QLabel, QLineEdit, QPushButton, + QVBoxLayout, QWidget) + +MIN_TEMP_C = 0 +MAX_TEMP_C = 200 +QUEUE_LEN = 5 + + +class TemperatureSensor(QWidget): + temperature_reading_signal: pyqtSignal = pyqtSignal(Temperature) + + def __init__(self) -> None: + super().__init__() + + self.temperature_reading_signal.connect(self.temperature_received) + self.temp_subscriber: GUIEventSubscriber = GUIEventSubscriber( + Temperature, + "temperature", + self.temperature_reading_signal + ) + + self.temps: deque[float] = deque(maxlen=QUEUE_LEN) + self.offset = 0.0 + + root_layout = QVBoxLayout() + self.setLayout(root_layout) + + self.ave_temp_label = QLabel() + self.ave_temp_label.setText('Waiting for temp...') + + self.offset_field = QLineEdit() + self.offset_field.setPlaceholderText('Enter offset (C)') + + self.offset_button = QPushButton('Set offset') + self.offset_button.clicked.connect(self.set_offset) + + root_layout.addWidget(self.ave_temp_label) + root_layout.addWidget(self.offset_field) + root_layout.addWidget(self.offset_button) + + def set_offset(self) -> None: + offset_text = self.offset_field.text() + + if offset_text == '': + offset_text = '0' + + try: + self.offset = float(offset_text) + self.offset_button.setText('Offset applied') + except ValueError: + self.offset_button.setText('Illegal') + return + + @pyqtSlot(Temperature) + def temperature_received(self, msg: Temperature) -> None: + if MIN_TEMP_C <= msg.reading <= MAX_TEMP_C: + self.temps.append(msg.reading) + + ave = sum(self.temps) / QUEUE_LEN + offset_ave = ave + self.offset + self.ave_temp_label.setText(f'{round(offset_ave, 4)}\t°C') + + self.offset_button.setText('Set offset') diff --git a/src/surface/gui/launch/operator_launch.py b/src/surface/gui/launch/operator_launch.py index 74f64835..1f07f4a3 100644 --- a/src/surface/gui/launch/operator_launch.py +++ b/src/surface/gui/launch/operator_launch.py @@ -15,6 +15,7 @@ def generate_launch_description() -> LaunchDescription: ("/surface/gui/mavros/cmd/command", "/tether/mavros/cmd/command"), ("/surface/gui/mavros/param/set", "/tether/mavros/param/set"), ("/surface/gui/mavros/param/pull", "/tether/mavros/param/pull"), + ("/surface/gui/temperature", "/tether/temperature"), ("/surface/gui/vehicle_state_event", "/surface/vehicle_state_event"), ("/surface/gui/mavros/cmd/arming", "/tether/mavros/cmd/arming"), ("/surface/gui/ip_address", "/tether/ip_address"),