Skip to content

Fix clearpath_tests so they don't crash/hang under certain conditions #220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: rc/jazzy/2.6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ad0005f
Add a new class to handle timeouts to reduce duplication
civerachb-cpr Jun 2, 2025
5577550
Remove node-specific mains, remove .start() from the TestNode class
civerachb-cpr Jun 2, 2025
ba90233
Implement timeout in MCU test so it doesn't hang forever
civerachb-cpr Jun 2, 2025
8007975
Remove unnecessary functions from canbus test
civerachb-cpr Jun 2, 2025
bc96275
Implement timeout in the diagnostic test
civerachb-cpr Jun 2, 2025
a911808
Implement the new timeout for estop tests. Add an abort() method to k…
civerachb-cpr Jun 2, 2025
4fb9f08
Remove unnecessary import
civerachb-cpr Jun 2, 2025
57e97aa
Remove start() method from estop test
civerachb-cpr Jun 2, 2025
a04e174
Implement new timeout in fan test. Call the abort method in the MCU test
civerachb-cpr Jun 2, 2025
dc04864
Implement new timeout in imu test
civerachb-cpr Jun 2, 2025
9db699c
Remove unused import
civerachb-cpr Jun 2, 2025
d0ff910
Implement new timeout in linear acceleration test
civerachb-cpr Jun 2, 2025
9eaaac0
Implement new timeout in rotation test
civerachb-cpr Jun 2, 2025
44c1481
Cleanup
civerachb-cpr Jun 2, 2025
95f8e92
Fix a parser error that occurs if wifi is turned off
civerachb-cpr Jun 6, 2025
7ff3470
Move motor currents initialization to constructor, call .start() in t…
civerachb-cpr Jun 6, 2025
068607b
Change how we calculate the IMU inclination; calculate the inclinatio…
civerachb-cpr Jun 9, 2025
01b3df9
Fix the logging for the linear acceleration test
civerachb-cpr Jun 9, 2025
38b839b
Fix changing from a single result to multiple
civerachb-cpr Jun 9, 2025
4f71425
Include the labels in the results
civerachb-cpr Jun 9, 2025
d51c40d
Fix the math for the error bars
civerachb-cpr Jun 9, 2025
09b729c
Remove whitespace before formatting
civerachb-cpr Jun 9, 2025
de60d1d
Test the direction & magnitude of the angular velocity separately. Fi…
civerachb-cpr Jun 9, 2025
52f3292
Allow long string
civerachb-cpr Jun 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions clearpath_tests/clearpath_tests/canbus_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@
import re
import subprocess

from clearpath_generator_common.common import BaseGenerator
from clearpath_tests.test_node import ClearpathTestNode, ClearpathTestResult

import rclpy


class CanbusTestNode(ClearpathTestNode):
"""Check the output of candump and count the number if IDs."""
Expand Down Expand Up @@ -68,15 +65,6 @@ def __init__(

self.test_name = f'CAN {self.can_interface}'

def start(self):
while True:
result = self.read_can_log()

if result.success:
self.get_logger().info(result.message)
else:
self.get_logger().warning(result.message)

def run_test(self):
self.get_logger().info(f'Collecting CAN messages on {self.can_interface}...')
result = self.read_can_log()
Expand Down Expand Up @@ -178,23 +166,3 @@ def get_test_result_details(self):

details += '\nDevices that do not use CANopen may be incorrectly identified in the list above' # noqa: E501
return details


def main():
setup_path = BaseGenerator.get_args()
rclpy.init()

ct = CanbusTestNode(setup_path=setup_path)

try:
ct.start()
rclpy.spin(ct)
except KeyboardInterrupt:
pass

ct.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()
47 changes: 10 additions & 37 deletions clearpath_tests/clearpath_tests/diagnostic_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,13 @@
import re

from clearpath_config.common.types.platform import Platform
from clearpath_generator_common.common import BaseGenerator
from clearpath_tests.test_node import ClearpathTestNode, ClearpathTestResult
from clearpath_tests.timer import Timeout

from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus

import rclpy
from rclpy.qos import qos_profile_system_default
from rclpy.time import Duration


PLATFORM_ANY = '*'
Expand Down Expand Up @@ -188,27 +187,21 @@ def diagnostic_callback(self, diagnostic_array):
elif status.level == DiagnosticStatus.STALE:
pass

def start(self):
self.diagnostc_sub = self.create_subscription(
DiagnosticArray,
f'/{self.namespace}/diagnostics',
self.diagnostic_callback,
qos_profile_system_default
)

def run_test(self):
results = []

self.test_in_progress = True
self.start()

# collect 30s worth of data
start_time = self.get_clock().now()
end_time = start_time + Duration(seconds=30.0)

self.get_logger().info('Collecting 30 seconds of diagnostic data...')
while self.get_clock().now() < end_time:
rclpy.spin_once(self)
self.diagnostc_sub = self.create_subscription(
DiagnosticArray,
f'/{self.namespace}/diagnostics',
self.diagnostic_callback,
qos_profile_system_default
)
timeout = Timeout(self, 30)
while not timeout.elapsed:
rclpy.spin_once(self, timeout_sec=1.0)

if len(self.warnings) == 0 and len(self.errors) == 0 and len(self.allowed_errors) == 0:
results.append(ClearpathTestResult(True, 'Diagnostics', 'No errors, no warnings'))
Expand Down Expand Up @@ -257,23 +250,3 @@ def get_test_result_details(self):
details += f'* {warn.name}: {warn.message}\n'

return details


def main():
setup_path = BaseGenerator.get_args()
rclpy.init()

dt = DiagnosticTestNode(setup_path)

try:
dt.start()
rclpy.spin(dt)
except KeyboardInterrupt:
pass

dt.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()
27 changes: 0 additions & 27 deletions clearpath_tests/clearpath_tests/drive_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
# POSSIBILITY OF SUCH DAMAGE.
import math

from clearpath_generator_common.common import BaseGenerator

from clearpath_tests.mobility_test import MobilityTestNode
from clearpath_tests.test_node import ClearpathTestResult

Expand Down Expand Up @@ -185,28 +183,3 @@ def run_test(self):
))

return results


def main():
setup_path = BaseGenerator.get_args()
rclpy.init()

try:
dt = DriveTestNode(setup_path)
dt.start()
try:
while not dt.test_done:
rclpy.spin_once(dt)
dt.get_logger().info('Test complete')
except KeyboardInterrupt:
dt.get_logger().info('User aborted! Cleaning up & exiting...')
dt.destroy_node()
except TimeoutError:
# This error is already logged when it's raised
pass

rclpy.shutdown()


if __name__ == '__main__':
main()
65 changes: 20 additions & 45 deletions clearpath_tests/clearpath_tests/estop_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@
import threading
import time

from clearpath_generator_common.common import BaseGenerator
from clearpath_tests.test_node import ClearpathTestNode, ClearpathTestResult
from clearpath_tests.timer import Timeout

from geometry_msgs.msg import TwistStamped
import rclpy
from rclpy.duration import Duration
from rclpy.qos import qos_profile_sensor_data, qos_profile_system_default
from std_msgs.msg import Bool

Expand Down Expand Up @@ -72,14 +71,6 @@ def estop_callback(self, msg: Bool):
state = 'clear'
self.get_logger().info(f'{self.estop_type} state: {state}')

def start(self):
self.estop_sub = self.create_subscription(
Bool,
f'/{self.namespace}/platform/emergency_stop',
self.estop_callback,
qos_profile_sensor_data
)

def run_test(self):
if self.optional:
user_input = self.promptYN(
Expand All @@ -90,7 +81,13 @@ def run_test(self):
return [ClearpathTestResult(None, self.test_name, 'Skipped; component not installed')] # noqa: E501

self.test_in_progress = True
self.start()

self.estop_sub = self.create_subscription(
Bool,
f'/{self.namespace}/platform/emergency_stop',
self.estop_callback,
qos_profile_sensor_data
)

self.cmd_vel_pub = self.create_publisher(
TwistStamped,
Expand All @@ -105,7 +102,7 @@ def run_test(self):
ui_thread = threading.Thread(target=self.run_ui)
ui_thread.start()
while not self.test_done:
rclpy.spin_once(self)
rclpy.spin_once(self, timeout_sec=1.0)
ui_thread.join()
return self.results

Expand Down Expand Up @@ -133,14 +130,14 @@ def run_ui(self):
return

# wait until we know the state of the e-stop
start_time = self.get_clock().now()
timeout = Duration(seconds=10)
timeout = Timeout(self, 10.0)
print(f'Getting {self.estop_type} status...')
while (
self.estop_engaged is None and
(self.get_clock().now() - start_time) < timeout
self.estop_engaged is None
and not timeout.elapsed
):
time.sleep(0.1)
timeout.abort()

if self.estop_engaged is None:
results.append(ClearpathTestResult(
Expand Down Expand Up @@ -240,9 +237,8 @@ def run_ui(self):
def command_wheels(self):
self.cmd_vel.twist.linear.x = 0.1

start_time = self.get_clock().now()
duration = Duration(seconds=2)
while (self.get_clock().now() - start_time) < duration:
timeout = Timeout(self, 2.0)
while not timeout.elapsed:
pass

self.cmd_vel.twist.linear.x = 0.0
Expand All @@ -256,33 +252,12 @@ def wait_for_estop(self, state, timeout_seconds=10):

@return True if the e-stop state is in the desired state, otherwise False
"""
start_time = self.get_clock().now()
now = self.get_clock().now()
timeout = Duration(seconds=timeout_seconds)
timeout = Timeout(self, timeout_seconds)
while (
self.estop_engaged != state and
(now - start_time) < timeout
self.estop_engaged != state
and not timeout.elapsed
):
now = self.get_clock().now()
pass
timeout.abort()

return self.estop_engaged == state


def main():
setup_path = BaseGenerator.get_args()
rclpy.init()

st = EstopTestNode('', setup_path=setup_path)

try:
st.start()
rclpy.spin(st)
except KeyboardInterrupt:
pass

st.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()
37 changes: 8 additions & 29 deletions clearpath_tests/clearpath_tests/fan_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from clearpath_generator_common.common import BaseGenerator
from clearpath_platform_msgs.msg import Fans, Status
from clearpath_tests.test_node import ClearpathTestNode, ClearpathTestResult
from clearpath_tests.timer import Timeout

import rclpy
from rclpy.duration import Duration
from rclpy.qos import qos_profile_sensor_data


Expand Down Expand Up @@ -65,10 +64,10 @@ def check_firmware_version(self):
"""
self.mcu_status = None

start_at = self.get_clock().now()
timeout_duration = Duration(seconds=10)
while self.get_clock().now() - start_at <= timeout_duration and self.mcu_status is None:
rclpy.spin_once(self)
timeout = Timeout(self, 10.0)
while not timeout.elapsed and self.mcu_status is None:
rclpy.spin_once(self, timeout_sec=1.0)
timeout.abort()

if self.mcu_status is None:
return (None, False)
Expand Down Expand Up @@ -134,10 +133,9 @@ def run_test(self):

def wait_3s():
self.get_logger().info('Waiting for fans to spin up/down...')
start = self.get_clock().now()
sleep_time = Duration(seconds=3)
while self.get_clock().now() - start < sleep_time:
rclpy.spin_once(self)
timeout = Timeout(self, 3.0)
while not timeout.elapsed:
rclpy.spin_once(self, timeout_sec=1.0)

for i in range(self.n_fans):
self.fan_msg.fans[i] = 0
Expand Down Expand Up @@ -172,22 +170,3 @@ def wait_3s():
results.append(ClearpathTestResult(False, 'Fans (all on)', None))

return results


def main():
setup_path = BaseGenerator.get_args()
rclpy.init()

fan_test = FanTestNode(setup_path)

try:
rclpy.spin(fan_test)
except KeyboardInterrupt:
pass

fan_test.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()
Loading