Skip to content

Commit

Permalink
Merge pull request #856 from uf-mil/network_kill
Browse files Browse the repository at this point in the history
Added network kill
  • Loading branch information
cbrxyz authored Sep 4, 2022
2 parents 14839c0 + 6acca56 commit c3b1e68
Show file tree
Hide file tree
Showing 27 changed files with 582 additions and 150 deletions.
14 changes: 9 additions & 5 deletions NaviGator/gnc/navigator_thrust_mapper/nodes/thrust_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ class ThrusterMapperNode:

def __init__(self):
self.is_vrx = rospy.get_param("/is_vrx", default=False)
self.is_sim = rospy.get_param("/is_simulation", default=False)

# Used for mapping wrench to individual thrusts
urdf = rospy.get_param("/robot_description", default=None)
if urdf is None or len(urdf) == 0:
raise Exception("robot description not set or empty")
if self.is_vrx:
if self.is_vrx or self.is_sim:
self.thruster_map = ThrusterMap.from_vrx_urdf(urdf)
else:
self.thruster_map = ThrusterMap.from_urdf(urdf)
Expand All @@ -38,10 +39,13 @@ def __init__(self):
self.wrench = np.zeros(3)

# Publisher for each thruster
if self.is_vrx:
thrust_string = 0
if self.is_vrx or self.is_sim:
if self.is_vrx:
thrust_string = 5
self.publishers = [
rospy.Publisher(
f"/wamv/thrusters/{name[5:]}_thrust_cmd",
f"/wamv/thrusters/{name[thrust_string:]}_thrust_cmd",
Float32,
queue_size=1,
)
Expand All @@ -55,7 +59,7 @@ def __init__(self):

# Joint state publisher
# TODO(ironmig):
if not self.is_vrx:
if not self.is_vrx and not self.is_sim:
self.joint_state_pub = rospy.Publisher(
"/thruster_states", JointState, queue_size=1
)
Expand Down Expand Up @@ -104,7 +108,7 @@ def publish_thrusts(self) -> None:
return
for i in range(len(self.publishers)):
commands[i].setpoint = thrusts[i]
if not self.is_vrx:
if not self.is_vrx and not self.is_sim:
for i in range(len(self.publishers)):
self.joint_state_msg.effort[i] = commands[i].setpoint
self.publishers[i].publish(commands[i])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .constants import constants
from .heartbeat_server import HeartbeatServer
from .simulated_kill_board import SimulatedKillBoard
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3

import rospy
from std_msgs.msg import Header, String


class HeartbeatServer:
def __init__(self, topic: str, period: float):
self.pub = rospy.Publisher(topic, Header, queue_size=10)

self.period = period
rospy.Timer(rospy.Duration(period / 2), self.publish)

def publish(self, *args):
self.pub.publish(Header())


if __name__ == "__main__":
rospy.init_node("heartbeat_server")
server = HeartbeatServer("/network", 1.0)
rospy.spin()
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import serial
from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
from mil_tools import thread_lock
from navigator_alarm_handlers import NetworkLoss
from navigator_kill_board import constants
from ros_alarms import AlarmBroadcaster, AlarmListener
from sensor_msgs.msg import Joy
Expand Down Expand Up @@ -59,6 +60,8 @@ def __init__(self):
for kill in constants["KILLS"]:
self.board_status[kill] = False
self.kills: list[str] = list(self.board_status.keys())
self.network_killed = False
self.software_killed = False
self.expected_responses = []
self.network_msg = None
self.wrench = ""
Expand Down Expand Up @@ -89,12 +92,16 @@ def __init__(self):

self._hw_kill_listener = AlarmListener("hw-kill", self.hw_kill_alarm_cb)
self._kill_listener = AlarmListener("kill", self.kill_alarm_cb)
self.kill_broadcaster = AlarmBroadcaster("kill")
self.kill_broadcaster.wait_for_server()
self._network_kill_listener = AlarmListener(
"network-loss", self.network_kill_alarm_cb
)
self._hw_kill_listener.wait_for_server()
self._kill_listener.wait_for_server()
self._network_kill_listener.wait_for_server()
rospy.Subscriber("/wrench/selected", String, self.wrench_cb)
rospy.Subscriber(
"/network", Header, self.network_cb
) # Passes along network hearbeat to kill board
self.network_kill = NetworkLoss()

def connect(self):
if rospy.get_param(
Expand Down Expand Up @@ -215,12 +222,13 @@ def handle_byte(self, msg):
if msg == byte:
del self.expected_responses[index]
return
# GH-861: Figure out why this happens so much
# Log a warning if an unexpected byte was received
rospy.logwarn(
"Received an unexpected byte {}, remaining expected_responses={}".format(
hex(ord(msg)), len(self.expected_responses)
)
)
# rospy.logwarn(
# "Received an unexpected byte {}, remaining expected_responses={}".format(
# hex(ord(msg)), len(self.expected_responses)
# )
# )

@thread_lock(lock)
def receive(self):
Expand Down Expand Up @@ -267,12 +275,30 @@ def wrench_cb(self, msg):
)
self.wrench = wrench

def network_cb(self, msg):
def network_kill_alarm_cb(self, alarm):
"""
Pings kill board on every network hearbeat message. Pretends to be the rf-based hearbeat because
real one does not work :(
"""
self.request(constants["PING"]["REQUEST"], constants["PING"]["RESPONSE"])
if alarm.raised:
self.network_killed = True
if self.software_killed:
return
self.kill_broadcaster.raise_alarm()
self.request(
constants["COMPUTER"]["KILL"]["REQUEST"],
constants["COMPUTER"]["KILL"]["RESPONSE"],
)
else:
self.network_killed = False
if self.software_killed:
return
self.kill_broadcaster.clear_alarm()
self.request(
constants["COMPUTER"]["CLEAR"]["REQUEST"],
constants["COMPUTER"]["CLEAR"]["RESPONSE"],
)
# self.request(constants["PING"]["REQUEST"], constants["PING"]["RESPONSE"])

def publish_diagnostics(self, err=None):
"""
Expand Down Expand Up @@ -343,21 +369,28 @@ def request(self, write_str, expected_response=None):
Returns True or False depending on the response.
With no `recv_str` passed in the raw result will be returned.
"""
self.ser.write(write_str)
self.ser.write(write_str.encode())
if expected_response is not None:
for byte in expected_response:
self.expected_responses.append(byte)
self.expected_responses.append(byte.encode())

def kill_alarm_cb(self, alarm):
"""
Informs kill board about software kills through ROS Alarms
"""
if alarm.raised:
self.software_killed = True
if self.network_killed:
return
self.request(
constants["COMPUTER"]["KILL"]["REQUEST"],
constants["COMPUTER"]["KILL"]["RESPONSE"],
)
else:
self.software_killed = False
if self.network_killed:
self.kill_broadcaster.raise_alarm()
return
self.request(
constants["COMPUTER"]["CLEAR"]["REQUEST"],
constants["COMPUTER"]["CLEAR"]["RESPONSE"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
<!-- Start kill_board_dirver -->
<include file="$(find navigator_launch)/launch/hardware/kill_board.launch" />

<test test-name="kill_board_test" pkg="navigator_kill_board" type="kill_board_test.py" time-limit="180.0"/>
<test test-name="kill_board_test_1" pkg="navigator_kill_board" type="kill_board_test_1.py" time-limit="180.0"/>
<test test-name="kill_board_test_2" pkg="navigator_kill_board" type="kill_board_test_2.py" time-limit="180.0"/>
<test test-name="kill_board_test_3" pkg="navigator_kill_board" type="kill_board_test_3.py" time-limit="180.0"/>
<test test-name="kill_board_test_4" pkg="navigator_kill_board" type="kill_board_test_4.py" time-limit="180.0"/>
</launch>
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
import unittest
from copy import copy, deepcopy
from threading import Lock

import rospy
from mil_tools import thread_lock
from navigator_alarm_handlers import NetworkLoss
from navigator_kill_board import HeartbeatServer
from ros_alarms import AlarmBroadcaster, AlarmListener
from std_msgs.msg import Header
from std_srvs.srv import SetBool

lock = Lock()


class killtest(unittest.TestCase):
def __init__(self, *args):
self.hw_kill_alarm = None
self.network_kill_alarm = None
self.hardware_updated = False
self.network_updated = False
self.AlarmListener = AlarmListener("hw-kill", self._hw_kill_cb)
self.NetworkListener = AlarmListener("network-loss", self._network_kill_cb)
self.AlarmBroadcaster = AlarmBroadcaster("kill")
self.network = NetworkLoss()
super().__init__(*args)
# self.heartbeat = HeartbeatServer("/network", 1.0)

@thread_lock(lock)
def reset_update(self):
"""
Reset update state to False so we can notice changes to hw-kill
"""
self.hardware_updated = False
self.network_updated = False

@thread_lock(lock)
def _hw_kill_cb(self, alarm):
"""
Called on change in hw-kill alarm.
If the raised status changed, set update flag to true so test an notice change
"""
if self.hw_kill_alarm is None or alarm.raised != self.hw_kill_alarm.raised:
self.hardware_updated = True
self.hw_kill_alarm = alarm

@thread_lock(lock)
def _network_kill_cb(self, alarm):
"""
Called on change in network-kill alarm.
If the raised status changed, set update flag to true so test an notice change
"""
if (
self.network_kill_alarm is None
or alarm.raised != self.network_kill_alarm.raised
):
self.network_updated = True
self.network_kill_alarm = alarm

def wait_for_kill_update(self, timeout=rospy.Duration(0.5), ver=None):
"""
Wait up to timeout time to an hw-kill alarm change. Returns a copy of the new alarm or throws if times out
"""
timeout = rospy.Time.now() + timeout
while rospy.Time.now() < timeout:
lock.acquire()
hardware_updated = copy(self.hardware_updated)
network_updated = copy(self.network_updated)
alarm = (deepcopy(self.hw_kill_alarm), deepcopy(self.network_kill_alarm))
lock.release()
if hardware_updated or network_updated:
return alarm
rospy.sleep(0.01)
raise Exception("timeout")

def assert_raised(self, timeout=rospy.Duration(1)):
"""
Waits for update and ensures it is now raised
"""
alarm = self.wait_for_kill_update(timeout)
hardware_pass = alarm[0].raised if alarm[0] is not None else True
network_pass = alarm[1].raised if alarm[1] is not None else True
self.assertEqual(hardware_pass and network_pass, True)

def assert_cleared(self, timeout=rospy.Duration(1)):
"""
Wait for update and ensures is now cleared
"""
alarm = self.wait_for_kill_update(timeout)
hardware_pass = alarm[0].raised if alarm[0] is not None else True
network_pass = alarm[1].raised if alarm[1] is not None else True
self.assertEqual(hardware_pass and network_pass, False)

def test_1_initial_state(self): # test the initial state of kill signal
"""
Tests initial state of system, which should have hw-kill raised because kill is raised at startup.
Because hw-kill will be initialized to cleared then later raised when alarm server is fully started,
so we need to allow for pottentialy two updates before it is raised.
"""
pub = rospy.Publisher("/network", Header, queue_size=10)
rospy.sleep(1)
pub.publish(Header())
self.reset_update()
alarm = self.wait_for_kill_update(
timeout=rospy.Duration(10.0), ver=1
) # Allow lots of time for initial alarm setup
if (alarm[0] and alarm[0].raised) or (alarm[1] and alarm[1].raised):
self.assertTrue(True)
return
self.reset_update()
self.assert_raised(timeout=rospy.Duration(10.0))


if __name__ == "__main__":
rospy.init_node("lll", anonymous=True)
import rostest

rostest.rosrun("navigator_kill_board", "kill_board_test_1", killtest)
unittest.main()
Loading

0 comments on commit c3b1e68

Please sign in to comment.