Skip to content

Commit f6d16bf

Browse files
committed
Implemented heartbeat_monitor
Utilises a custom QoS profile to implement the heartbeat timing behaviour. Assumes a service would be available to halt and resume all rover behaviour as a safety mechanism.
1 parent 2b06732 commit f6d16bf

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

tutorials/heartbeat_monitor.py

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
Monash Nova Rover Team
5+
6+
This class is a class to monitor the heartbeart signal from
7+
the base station to ensure connection has been maintained with
8+
the base station. Where a heart beat is missed an appropriate
9+
message is logged. Where the heart beat is lost, a call is made
10+
to an appropriate service to halt the rover and indicate loss visually (i.e. LED strip)
11+
12+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13+
NODE: heartbeat_monitor
14+
TOPICS:
15+
- None
16+
SERVICES:
17+
- None
18+
ACTIONS:
19+
- None
20+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21+
PACKAGE: tutorials
22+
AUTHOR(S): Josh Cherubino
23+
CREATION: 31/08/21
24+
EDITED: 31/08/21
25+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26+
TODO:
27+
- tune BEAT_MISSED_DEADLINE and BEAT_LOST_DURATION values
28+
- Instantiate and call client once node has been created
29+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30+
"""
31+
32+
33+
import rclpy
34+
from rclpy.node import Node
35+
from std_msgs.msg import Empty
36+
37+
#define constants
38+
#max allowed duration between heartbeats.
39+
#failure to meet this deadline indicates a heartbeat missed but
40+
#not a complete failure.
41+
BEAT_MISSED_DEADLINE = rclpy.duration.Duration(seconds=1/50.0)
42+
43+
#max allowed duration before heartbeat is considered lost (heartbeat publisher considered
44+
#to have lost its liveliness from QoS perspective
45+
BEAT_LOST_DURATION = rclpy.duration.Duration(seconds=1/25.0)
46+
47+
class HeartbeatMonitor(Node):
48+
def __init__(self) -> None:
49+
"""
50+
Initialise HeartbeatMonitor instance
51+
"""
52+
#use control namespace for node
53+
#don't need parameters for this node so do not run.
54+
super().__init__('heartbeat_monitor', namespace='control',
55+
start_parameter_services=False)
56+
57+
#configure custom QoS profile for subscriber to handle timing issues and register
58+
#events when heartbeat not achieved
59+
#set history to keep last so depth parameter is used (should be default)
60+
#depth 1 as only need latest message
61+
qos_profile = rclpy.qos.QoSProfile(history=rclpy.qos.QoSHistoryPolicy.KEEP_LAST,
62+
depth=1)
63+
64+
#use best effort to be flexible (allow pub to either be best effort or reliable)
65+
#https://docs.ros.org/en/foxy/Concepts/About-Quality-of-Service-Settings.html#qos-policies
66+
qos_profile.reliability = rclpy.qos.QoSReliabilityPolicy.BEST_EFFORT
67+
68+
#Samples should not be persistent
69+
#N.B. if publisher chooses to make them consistent then this is still allowed
70+
qos_profile.durability = rclpy.qos.QoSDurabilityPolicy.VOLATILE
71+
72+
#automatic liveliness detection so automatically detect when lease duration exceeded
73+
qos_profile.liveliness = rclpy.qos.QoSLivelinessPolicy.AUTOMATIC
74+
#N.B. for QoS compatability a publisher must promise a QoS with
75+
#lease duration shorter than the lease expected here
76+
qos_profile.liveliness_lease_duration = BEAT_LOST_DURATION
77+
78+
#set deadline for each beat. if deadline not met log warning
79+
#N.B. that for QoS compatability a publisher must promise a QoS
80+
#with deadline shorter than the deadline expected in this subscriber
81+
qos_profile.deadline = BEAT_MISSED_DEADLINE
82+
83+
#create event callbacks to be triggerred on deadline missed
84+
#and lease duration execeeded (liveliness lost)
85+
#this isn't documented for some reason.
86+
#see source https://github.com/ros2/rclpy/blob/98707238ad2564e28cbc987fc3a31ed9a1c86243/rclpy/rclpy/qos_event.py
87+
subscription_event_callbacks = \
88+
rclpy.qos_event.SubscriptionEventCallbacks(deadline=self.beat_missed,
89+
liveliness=self.check_beat_lost)
90+
91+
#create subscription with custom QoS profile and QoS callbacks.
92+
self.subscription = self.create_subscription(Empty, 'heartbeat',
93+
self.beat_received, qos_profile, event_callbacks=subscription_event_callbacks)
94+
95+
#flag to track if we have the heart beat.
96+
#Assume no heartbeat initially
97+
self.beat = False
98+
#flag to track if we have halted the rover previously
99+
self.halted = True
100+
101+
#create client instance here
102+
103+
#and initially halt rover until we confirm we can receive a heartbeat
104+
105+
self.get_logger().info('Heartbeat monitor started')
106+
107+
def beat_received(self, msg: Empty) -> None:
108+
"""
109+
Callback to update state when a heartbeat is received from topic.
110+
If rover was previously halted then will resume the rovers behaviour
111+
Returns None
112+
:param msg: Empty message received from publisher
113+
"""
114+
#if heartbeat just re-gained, log that it has been detected
115+
#otherwise do nothing
116+
if not self.beat:
117+
self.get_logger().info('Heartbeat detected')
118+
self.beat = True
119+
#heart-beat regained
120+
if self.halted:
121+
#call client to resume rover actions
122+
#self.rover_emergency_client.call_async(Msg(RESUME))
123+
self.get_logger().info('Resuming rover')
124+
self.halted = False
125+
126+
def beat_missed(self, info: rclpy.qos_event.QoSRequestedDeadlineMissedInfo) -> None:
127+
"""
128+
Event callback run when publisher misses deadline required by QoS profile.
129+
Sets heartbeat as lost and logs warning
130+
:param info: Information about the requested deadline miss
131+
"""
132+
#only log missed beat (to avoid spam where heartbeat completely lost) when
133+
#it was previously not lost
134+
if self.beat:
135+
self.get_logger().warn('Heartbeat missed. Total misses: {}'.format(info.total_count))
136+
self.beat = False
137+
138+
def check_beat_lost(self, info: rclpy.qos_event.QoSLivelinessChangedInfo) -> None:
139+
"""
140+
Event callback run when publisher liveliness state changes
141+
Checks changed publisher liveliness, and if 'not_alive', logs error and
142+
calls halt_rover service to prevent any damage to rover
143+
:param info: Information about the liveliness change event
144+
"""
145+
#Beat only lost where our 'not_alive_count' equal to or exceeds 'alive_count'
146+
#(b/c we are initially in alive state first time we receive message)
147+
if info.not_alive_count >= info.alive_count:
148+
#call appropriate service to halt rovers motion here
149+
#i.e. self.future = self.rover_emergency_client.call_async(Msg(HALT))
150+
#set state to halted so we can resume when beat re-acquired
151+
self.halted = True
152+
self.beat = False
153+
self.get_logger().error('Heartbeat lost. Halting rover')
154+
155+
def main(args=None):
156+
rclpy.init(args=args)
157+
158+
heartbeat_monitor = HeartbeatMonitor()
159+
160+
rclpy.spin(heartbeat_monitor)
161+
162+
rclpy.shutdown()
163+
164+
if __name__ == '__main__':
165+
main()
166+
167+

0 commit comments

Comments
 (0)