@@ -13,7 +13,12 @@ import { IconClock, IconNetwork, IconNetworkOff } from "@tabler/icons-react"
1313
1414// Redux
1515import { useSelector } from "react-redux"
16- import { selectAlt , selectBatteryData } from "../../redux/slices/droneInfoSlice"
16+ import {
17+ selectAlt ,
18+ selectBatteryData ,
19+ selectHeartbeatLastReceivedAt ,
20+ } from "../../redux/slices/droneInfoSlice"
21+ import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice"
1722import { selectIsConnectedToSocket } from "../../redux/slices/socketSlice"
1823
1924// Helper imports
@@ -36,9 +41,11 @@ export function StatusSection({ icon, value, tooltip }) {
3641
3742export default function StatusBar ( props ) {
3843 const isConnectedToSocket = useSelector ( selectIsConnectedToSocket )
44+ const isConnectedToDrone = useSelector ( selectConnectedToDrone )
3945 const [ time , setTime ] = useState ( moment ( ) )
4046 const batteryData = useSelector ( selectBatteryData )
4147 const alt = useSelector ( selectAlt )
48+ const heartbeatLastReceivedAt = useSelector ( selectHeartbeatLastReceivedAt )
4249
4350 // Update clock every second
4451 useEffect ( ( ) => {
@@ -50,6 +57,7 @@ export default function StatusBar(props) {
5057 const { getSetting } = useSettings ( )
5158 const { dispatchAlert, dismissAlert } = useAlerts ( )
5259 const highestAltitudeRef = useRef ( 0 )
60+ const heartbeatAlertActiveRef = useRef ( false )
5361
5462 useEffect ( ( ) => {
5563 const maxAltitude = getSetting ( "Dashboard.maxAltitudeAlert" )
@@ -126,6 +134,69 @@ export default function StatusBar(props) {
126134 } )
127135 } , [ batteryData ] )
128136
137+ useEffect ( ( ) => {
138+ const timeoutSeconds = Number (
139+ getSetting ( "Dashboard.heartbeatTimeoutSeconds" ) ?? 10 ,
140+ )
141+ const timeoutMs = Number . isFinite ( timeoutSeconds )
142+ ? timeoutSeconds * 1000
143+ : 10000
144+
145+ const checkHeartbeatTimeout = ( ) => {
146+ if ( ! isConnectedToDrone ) {
147+ if ( heartbeatAlertActiveRef . current ) {
148+ dismissAlert ( AlertCategory . Heartbeat )
149+ heartbeatAlertActiveRef . current = false
150+ }
151+ return
152+ }
153+
154+ if ( heartbeatLastReceivedAt <= 0 ) {
155+ if ( heartbeatAlertActiveRef . current ) {
156+ dismissAlert ( AlertCategory . Heartbeat )
157+ heartbeatAlertActiveRef . current = false
158+ }
159+ return
160+ }
161+
162+ const elapsedSeconds = Math . max (
163+ 0 ,
164+ Math . floor ( ( Date . now ( ) - heartbeatLastReceivedAt ) / 1000 ) ,
165+ )
166+ const isHeartbeatStale = elapsedSeconds * 1000 > timeoutMs
167+
168+ if ( isHeartbeatStale ) {
169+ dispatchAlert ( {
170+ category : AlertCategory . Heartbeat ,
171+ severity : AlertSeverity . Red ,
172+ dismissable : false ,
173+ jsx : (
174+ < >
175+ Caution! It has been { elapsedSeconds } s since the last heartbeat.
176+ </ >
177+ ) ,
178+ } )
179+ heartbeatAlertActiveRef . current = true
180+ return
181+ }
182+
183+ if ( heartbeatAlertActiveRef . current ) {
184+ dismissAlert ( AlertCategory . Heartbeat )
185+ heartbeatAlertActiveRef . current = false
186+ }
187+ }
188+
189+ checkHeartbeatTimeout ( )
190+ const id = setInterval ( checkHeartbeatTimeout , 1000 )
191+ return ( ) => clearInterval ( id )
192+ } , [
193+ dismissAlert ,
194+ dispatchAlert ,
195+ getSetting ,
196+ heartbeatLastReceivedAt ,
197+ isConnectedToDrone ,
198+ ] )
199+
129200 return (
130201 < div className = { `${ props . className } flex flex-col items-end` } >
131202 < div
0 commit comments