|
| 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