12 Geofencing and Fail-Safes: Designing for the Unexpected
The previous chapter ended on a deliberately loose thread. A geofence polygon had been drawn in QGroundControl, stored in the .plan file, decoded vertex by vertex, uploaded to the simulated vehicle — and then ignored. Nothing happened when the aircraft crossed it, because nothing had been told to happen. The fence was data without consequences, and the mission’s state machine carried a matching hole: a failsafe_event() check evaluated first on every tick, wired to nothing. This chapter closes both gaps. It arms the fence, fills the failsafe slot, and more generally takes up the discipline the whole Part has been circling: designing flight software for the flights that go wrong. The happy path — waypoints reached, battery ample, links alive — needs remarkably little engineering once the previous three chapters are absorbed. What separates a demonstration from a system is everything else: the gust that carries the vehicle toward the boundary, the battery that reads 28% with the aircraft 200 metres from home, the mission process that dies mid-flight. None of these is exotic. Each will happen, on a long enough sequence of flights, with probability one — and the entire craft of safety engineering is arranging, in advance, that when it happens the aircraft does something boring.
There is also an older thread to pick up. The Airspace Law chapter closed by observing that every rule it covered — the altitude ceiling, the authorized operating area, the rehearsed emergency procedure — was enforced by human discipline, and promised that this chapter would revisit those same constraints as software: coordinates the autopilot refuses to leave, thresholds compiled into parameters, emergency procedures that execute without waiting for a human to remember them. That promise is the right frame for everything below. A geofence is airspace law translated into a boundary check; a battery failsafe is the fuel-reserve rule translated into a threshold comparison; and the layered-margin doctrine — the organization’s 10-foot ceiling sitting far beneath the FAA’s 400-foot (122 m) one — reappears here, exactly, as mission thresholds sitting far above the firmware’s own floors.
12.1 Learning Objectives
- Configure and arm a geofence — a boundary polygon or radius plus an altitude ceiling — so that a breach triggers a chosen automatic action, and demonstrate the trigger in software-in-the-loop simulation.
- Implement the boundary check itself: a point-in-polygon test by ray casting against live estimated position, extended to a fenced volume by an independent altitude comparison.
- Distinguish a hard failsafe (an automatic, authority-seizing response such as Return-to-Launch or immediate landing) from a soft warning, and choose between the graded responses — warn, hover in place, return, land, terminate — for a given trigger.
- Size battery failsafe thresholds from mission geometry: compute the worst-case cost of the trip home and justify a Return-to-Launch trigger and a land floor against it.
- Read the running example’s
failsafe_monitor.pyand explain its structure — an independent watchdog node with thresholds as parameters, a 1 Hz evaluation tick, and a publish-while-active protocol — and implement the evaluation logic its scaffold leaves open. - Trace the override authority chain of a multi-node autonomy system from the safety pilot’s transmitter down to the behavior nodes, and state the design rule that overrides flow strictly downward.
12.2 Key Vocabulary
- Geofence — a software-defined boundary the vehicle must respect — an inclusion polygon or a radius about home for the horizontal plane, plus an altitude ceiling — checked continuously against the estimated position and wired to an automatic action on breach; introduced as
.plandata in the previous chapter, armed here. - Breach action — the configured response to a geofence violation, chosen from a graded menu: log only, warn the operator, hold position, return to launch, land immediately, or terminate flight.
- Point-in-polygon test — the geometric core of a polygonal fence check: cast a ray from the test position and count polygon-edge crossings — an odd count means inside, an even count outside.
- Failsafe — a pre-programmed automatic response to a detected fault or rule violation — link loss, low battery, boundary breach — designed so that the vehicle’s default behavior under failure is a safe one rather than a continuation of the mission.
- Hard failsafe — a failsafe that seizes authority when it fires: the triggering condition changes the flight mode itself (RTL, land, hold), and the mission software’s consent is neither requested nor required.
- Soft warning — an advisory response to an approaching limit — a margin fence, a battery caution — that informs the operator or mission logic while leaving authority where it is; the graceful layer that fires before the hard one must.
- Hover-in-place — the gentlest hard response: the vehicle stops and holds its current position (PX4’s Hold mode, a commanded loiter), buying time for a human decision without moving through any more of the world.
- Battery failsafe — the graded response ladder tied to remaining charge: a warning threshold, a return threshold sized to cover the worst-case trip home, and a land floor below which the only safe destination is straight down.
- Watchdog — an independent process that monitors a system it does not control, holding no mission state and sharing no failure mode with the logic it supervises; the running example’s
failsafe_monitornode is one. - Override authority — the explicit ordering of who may preempt whom in a multi-node system — safety pilot above firmware, firmware above watchdog, watchdog above mission logic, mission logic above behaviors — with overrides flowing strictly downward.
12.3 Arming the Fence
The previous chapter’s .plan file stored its geofence as four latitude/longitude vertices and an "inclusion": true flag — a region the vehicle must stay inside. Turning that stored polygon into an enforced boundary takes one decision, not more code: telling the autopilot what to do about a breach. PX4 exposes the choice as a parameter — the geofence action — with a graded menu: do nothing but log; warn the operator over telemetry; hold position at the boundary; fly Return-to-Launch; land immediately; or terminate flight, cutting the motors outright. QGroundControl presents the same menu in its safety settings, and uploads the fence geometry alongside the mission over the same MAVLink session. With the polygon uploaded and the action set to RTL, the fence from the previous chapter stops being documentation. The firmware now checks the estimated position against the boundary continuously — and, in modern PX4, checks a short prediction of the trajectory as well, so a vehicle flying fast at the fence is turned back before the line rather than after it. Even with no polygon uploaded at all, two parameters provide a minimal fence — a maximum horizontal distance from home and a maximum altitude — which is exactly the shape the running example’s own watchdog will adopt later in this chapter.
The graded menu deserves a moment, because choosing from it is a genuine design decision with the same structure every failsafe response will have. Warning preserves all authority and all risk: appropriate only when a human is watching and able to act. Hold — hover in place — is the gentlest seizure of authority: the vehicle stops moving through the world, which converts a developing situation into a static one, at the price of hovering indefinitely on a draining battery. Return trades airspace for energy: the vehicle leaves the situation entirely, along a predictable path, at a known cost. Land trades everything for immediacy: minimum time to the ground, wherever the ground happens to be. And termination is the response of last resort — a falling aircraft is the certain cost, paid to bound the harm a flying one might do; it exists for the case where continued powered flight is itself the hazard. The right choice depends on what surrounds the fence: a boundary with open field beyond it can afford RTL; a boundary shared with a crowd line cannot afford anything that keeps flying toward it. For the lawnmower survey of the previous chapter — a fenced grass field, nothing beyond the boundary but more grass — RTL is the standard and correct choice, and it is the choice the simulation exercise at the end of this chapter demonstrates: fly at the fence on purpose, and watch the mission end and the trip home begin.
12.3.1 The boundary check itself
The firmware does this checking for you, and the running example’s watchdog will do it again independently — so it is worth owning the few lines of geometry involved. The horizontal question is: given the estimated position and a polygon of \(m\) vertices, is the point inside? The standard answer is the ray-casting test, drawn in Figure 12.1: cast a ray from the test point in any fixed direction (east, by convention) and count how many polygon edges it crosses. A point inside the polygon must cross the boundary an odd number of times to escape to infinity; a point outside crosses an even number of times (possibly zero). The parity of the count is the answer, for any simple polygon, convex or not.
In code, working in the local metric coordinates the previous chapter’s conversion produces (east and north in metres about home):
def inside(fence, e, n):
"""Ray-cast point-in-polygon: fence is a list of (east, north)
vertices; (e, n) is the position under test. Casts a ray east
and counts edge crossings — odd means inside."""
crossings_odd = False
m = len(fence)
for i in range(m):
e1, n1 = fence[i]
e2, n2 = fence[(i + 1) % m]
if (n1 > n) != (n2 > n): # edge spans the ray's northing
e_cross = e1 + (n - n1) * (e2 - e1) / (n2 - n1)
if e_cross > e: # crossing lies east of the point
crossings_odd = not crossings_odd
return crossings_oddEach edge is examined once, so the test is \(O(m)\) — for the four-vertex fence of the previous chapter, four comparisons and at most four multiplications, comfortably nothing at any update rate a position estimate arrives at. Three practical points turn the textbook test into a fence worth trusting. First, the fence is a volume, not a polygon: the horizontal test is paired with an independent altitude comparison against a ceiling, and breach of either is a breach — that ceiling is where the layered-margin doctrine lands in code, set not at the 122 m legal maximum but at the mission’s own working limit, the same way an organization’s 10-foot training ceiling sat far below the FAA’s number. Second, the test runs against an estimate, and the previous chapter’s acceptance-radius lesson applies unchanged: the position carries metre-scale scatter, so a vehicle hovering exactly on the boundary would flicker between inside and outside at the noise rate. The standard repairs are a margin and a debounce — which is the third point: build two fences. A soft fence, shrunk inward from the hard one by a few metres, fires a warning (to the operator, to the mission logic) while everything is still legal; the hard fence fires the action. The gap between them is bought room for the graceful response — the mission node can turn the vehicle around on its own terms inside the margin, and the hard failsafe becomes what its name should always mean: the layer that fires only when the layer above it has already failed.
12.4 A Taxonomy of Fail-Safes
The geofence is one instance of a general pattern, and naming the pattern organizes everything the firmware offers. A failsafe is a trigger bound to a response. The triggers PX4 monitors continuously: loss of the RC link; loss of the data link to the ground station; loss of the Offboard setpoint stream — the dead-man’s switch of two chapters standing, now recognizable as one row of a larger table; low, critical, and emergency battery; geofence breach; and estimator failure, the EKF losing a usable position fix. The responses are the graded menu already met: warn, hold, return, land, terminate. The engineering content is in the binding — which response suits which trigger — and in one distinction that runs through the whole table: a hard failsafe changes the flight mode itself when it fires, seizing authority from whatever was flying the vehicle, while a soft warning informs and leaves authority in place. The previous chapter’s state machine already encoded where hard failsafes live in mission logic: checked first, every tick, outranking every state. What it left open — what fires the check — is precisely this table.
Return-to-Launch, the workhorse hard response, is itself a small mission: climb to a configured return altitude (so the path home clears whatever the vehicle was working beneath), fly to home — the position recorded at arming, with everything the previous chapter said about that — descend, and land. Its predictability is its virtue and its trap. The virtue: everyone on the crew knows, before the flight, exactly what the aircraft will do when something fails, and can stand where that path is not. The trap: RTL flies a straight line home through whatever happens to be in the way, which is why the return altitude is a safety parameter and not a convenience, and why a fence breach at the far side of a field must leave enough battery to fly the whole diagonal back.
That last clause is a calculation, and it sizes the battery failsafe. The running example’s pack stores 77 Wh, and the loaded aircraft draws about 365 W in mission flight — the numbers carried since Part I — so one percent of charge is about 0.77 Wh, or 7.6 seconds of flight. Now price the worst-case trip home for the watchdog’s fence geometry (a 200 m radius about home, a 30 m ceiling — the parameters met in the next section): 200 m of return cruise at 5 m/s is 40 s; the climb to return altitude and a metre-per-second descent from 30 m add roughly another 40–50 s; call the trip 90 seconds, about 12% of the pack. A Return-to-Launch trigger at 30% remaining therefore buys the worst-case trip home twice over — margin for a headwind, a detour, a slow human noticing — and a land floor at 15% marks the honest boundary below which the trip home is no longer affordable at all: under two minutes of flight remain, the cells are approaching the steep knee of the discharge curve where indicated charge falls fastest, and the only destination that is guaranteed reachable is straight down. Figure 12.2 draws the ladder, and its most important feature is what sits beneath the mission thresholds: the firmware’s own defaults — warn near 15%, act near 7%, land near 5% — which protect the aircraft, not the mission. The mission thresholds are set far above them deliberately, and the doctrine is the Airspace Law chapter’s, verbatim: the platform’s floor is where your margins begin, not where they end.
Two remaining rows of the table complete the taxonomy. Link-loss failsafes answer the question “who is flying this aircraft?” when the answer becomes “nobody”: RC loss and data-link loss each trigger a configurable response after a configurable delay, with RTL the common choice — and the Offboard stream timeout from two chapters ago is the same mechanism applied to a companion computer, with the vital property that the absence of messages is the trigger, so no failed component needs to succeed at announcing its own failure. Estimator failure is the humbling row: every response above assumes the vehicle knows where it is, and RTL toward a hallucinated home position is worse than useless, so a vehicle that loses its position fix falls back to responses that need progressively less state — attitude-stabilized descent, ultimately termination. It is the table’s reminder that failsafes are software running on the same estimates as everything else, which is exactly the argument for the independent monitor the next section builds: not because the firmware’s table is deficient, but because the mission has rules the firmware cannot know.
12.5 The Running Example’s Watchdog: failsafe_monitor.py
Everything so far lives in the firmware, guarding limits any aircraft shares: batteries, links, fences. But the Goose Management System has safety rules PX4 has never heard of — a standoff distance from a living animal, a mission battery budget stricter than the firmware’s, an altitude ceiling set by policy rather than physics — and enforcing mission rules takes mission software. The architecture Part I sketched for the running example includes a dedicated node for exactly this: a Failsafe node that continuously monitors mission safety — battery state and safe separation among its concerns — and, when an unsafe condition is detected, can override the active controller and trigger a protective action: hover, Return-to-Launch, or mission abort. Here is the node, in full — the real code, not a sketch:
"""
Failsafe Monitor
Independent mission specific watchdog
Publishes to /goose/failsafe that triggers various overrides
"""
import math
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from px4_msgs.msg import VehicleLocalPosition, BatteryStatus, VehicleStatus
from goose_ops.px4_defs import (PX4_QOS, T_VEHICLE_LOCAL_POSITION,
T_BATTERY, T_VEHICLE_STATUS, T_FAILSAFE)
class FailsafeMonitor(Node):
def __init__(self):
super().__init__('failsafe_monitor')
self.declare_parameter('battery_rtl', 0.30)
self.declare_parameter('battery_land', 0.15)
self.declare_parameter('geofence_radius_m', 200.0)
self.declare_parameter('max_altitude_m', 30.0)
self.pos = None
self.batt = None
self.status = None
self._last_reason = None
self.create_subscription(VehicleLocalPosition,
T_VEHICLE_LOCAL_POSITION,
self._on_lpos, PX4_QOS)
self.create_subscription(BatteryStatus, T_BATTERY,
self._on_batt, PX4_QOS)
self.create_subscription(VehicleStatus, T_VEHICLE_STATUS,
self._on_status, PX4_QOS)
self.pub = self.create_publisher(String, T_FAILSAFE, 10)
self.create_timer(1.0, self._tick)
self.get_logger().info('failsafe_monitor up')
def _on_lpos(self, m): self.pos = m
def _on_batt(self, m): self.batt = m
def _on_status(self, m): self.status = m
def _evaluate(self):
return None
def _tick(self):
reason = self._evaluate()
if reason is not None:
if reason != self._last_reason:
self.get_logger().error(f'FAILSAFE: {reason}')
self.pub.publish(String(data=reason))
elif self._last_reason is not None:
self.get_logger().info('failsafe condition cleared')
self._last_reason = reason
def main():
rclpy.init()
rclpy.spin(FailsafeMonitor())
if __name__ == '__main__':
main()Read it as three design decisions, because that is what it is. First, independence. The node is a separate process holding no mission state — it does not know whether the vehicle is searching, engaging, or returning, and does not care. It subscribes to three PX4 telemetry streams (local position, battery status, vehicle status — the same uORB-over-DDS topics the previous chapters established) and publishes one string. A crash in the mission logic cannot take it down; a bug in a behavior node cannot corrupt its view of the battery. This is the modularity argument from Part I’s closing chapter made concrete: the safety monitor must not share a failure mode with the logic it supervises, and the only way to guarantee that is a process boundary. Second, thresholds as parameters, not constants: battery_rtl at 0.30, battery_land at 0.15, a geofence_radius_m of 200 and a max_altitude_m of 30 are declared as ROS 2 parameters, so the field crew can tighten them per site — a smaller field, a lower ceiling — without touching code, exactly the property that made the autopilot’s own parameter system the right home for NAV_ACC_RAD in the previous chapter. (Note the fence shape: a radius about home and a ceiling — a cylinder in the local NED frame where home is the origin, so the whole horizontal check is one hypotenuse. The polygon test earlier in this chapter is what this becomes when the site demands a shape; the cylinder is what it is when 200 m of grass in every direction makes the shape irrelevant.) Third, the publish protocol, which lives in _tick and repays close reading: while a condition holds, the reason string is published every second — a continuous assertion, not a single event, so a subscriber that starts late or drops a message still hears about an active failsafe within a tick; the log gets one FAILSAFE: line when the reason first appears and one cleared line when it goes away, the same latching discipline (_last_reason) that keeps a persistent condition from flooding the log at 1 Hz.
And then the fourth thing, which is not a decision but a seam: _evaluate returns None. The shipped file is the watchdog’s scaffold — subscriptions, parameters, tick, protocol, all the plumbing that makes a monitor trustworthy — with the monitored conditions themselves left as the open socket (note import math, sitting unused at the top of the file, waiting). Everything this chapter has built is the material for filling it:
def _evaluate(self):
"""Return 'SEVERITY:reason' for the worst active condition, else
None. Rules are ordered worst-first, so the first match wins."""
p = self.get_parameter
if self.batt is not None:
if self.batt.remaining < p('battery_land').value:
return 'LAND:battery below land floor'
if self.pos is not None:
if math.hypot(self.pos.x, self.pos.y) > p('geofence_radius_m').value:
return 'RTL:geofence breach'
if -self.pos.z > p('max_altitude_m').value:
return 'RTL:altitude above ceiling'
if self.batt is not None:
if self.batt.remaining < p('battery_rtl').value:
return 'RTL:battery below mission threshold'
return NoneThe details carry the chapter’s lessons. BatteryStatus.remaining is a fraction of full charge, compared directly against the parameter thresholds of Figure 12.2. The position is PX4’s local NED estimate — north, east, down, metres from home — so altitude is \(-z\), the sign convention that has been earning its keep since the coordinate discussion of Part I, and the horizontal fence check is math.hypot of the two horizontal components against the radius. The ordering is a severity sort: a vehicle that is simultaneously outside the fence and below the land floor must land, not fly home, so LAND conditions are tested before any RTL condition. The reason strings are deliberately static: fold the live battery percentage into the string and it would change on every percent step, defeating the _last_reason latch and turning the one-line log into a stream — the tick’s latching discipline works only if “same condition” means “same string.” And the returned string is not prose — it is a tiny protocol, SEVERITY:reason, whose consumer is the next section’s subject.
12.7 Check Your Understanding
- A fence polygon has vertices, in local metres (east, north): (0, 0), (100, 10), (90, 80), (−10, 70). Using the ray-casting test with a ray cast east, determine whether the point (40, 40) is inside. Which edges does the ray cross?
The ray from (40, 40) runs east at northing 40. Test each edge for spanning \(n = 40\): edge (0,0)→(100,10) spans northings 0–10, no; edge (100,10)→(90,80) spans 10–80, yes — crossing at \(e = 100 + (40-10)\times(90-100)/(80-10) = 100 - 4.3 \approx 95.7\), which is east of 40, so it counts; edge (90,80)→(−10,70) spans 70–80, no; edge (−10,70)→(0,0) spans 0–70, yes — crossing at \(e = -10 + (40-70)\times(0-(-10))/(0-70) \approx -5.7\), which is west of 40, so it does not count. One eastward crossing — odd — so (40, 40) is inside the fence.
- The same mission is protected by a QGC geofence with the breach action set to RTL and by the running example’s
failsafe_monitorpublishingRTL:geofenceon breach. These look redundant. Give two concrete failure scenarios in which exactly one of the two layers catches the problem, and state the general principle.
Firmware-only catch: the companion computer crashes or hangs entirely — the watchdog node is dead and publishes nothing, but PX4’s fence check runs on the flight controller and fires regardless (as does the Offboard dead-man’s switch). Watchdog-only catch: a mission rule the firmware cannot know — the watchdog’s fence is deliberately tighter than the uploaded one (or the crew set geofence_radius_m down for a small site without re-uploading firmware fence geometry), or a mission-specific condition such as a separation distance fires — the firmware sees a perfectly legal flight while the mission layer correctly aborts. The principle is layered enforcement with independent failure modes: the firmware layer survives companion-computer death; the mission layer encodes rules above the firmware’s floor. Neither substitutes for the other, which is why both are armed.
- Using the running example’s numbers — 77 Wh pack, 365 W mission draw, fence radius 200 m, ceiling 30 m, return cruise 5 m/s — a teammate proposes lowering
battery_rtlfrom 0.30 to 0.18 to buy more search time. Evaluate the proposal quantitatively.
One percent of the pack is \(0.77\ \text{Wh} / 365\ \text{W} \approx 7.6\) s of flight. The worst-case return — 200 m at 5 m/s (40 s) plus climb and a descent from 30 m (roughly 40–50 s) — costs about 90 s, i.e. about 12% of the pack. Triggering RTL at 18% means arriving in the landing phase near \(18 - 12 = 6\%\) — at or below the firmware’s own critical threshold (~7%), inside the steep knee of the discharge curve where indicated charge falls fastest and voltage sag under load is worst, with zero margin for a headwind or a delayed trigger. The 30% setting lands the aircraft near 18% — above the 15% land floor with the worst case already paid. The proposal converts about 90 s of extra search into the elimination of the entire safety margin; reject it, or fund the search time honestly with a larger pack or a tighter fence (a smaller radius shrinks the worst-case return and legitimately lowers the defensible threshold).
- In
failsafe_monitor.py, the reason string is re-published every tick while the condition holds, but logged only when it changes; and in the mission manager, the first failsafe message latches the FAILSAFE state, which is terminal. Explain what each of the three choices (continuous publish, change-only logging, latched terminal state) protects against.
Continuous publish protects against lost messages and late joiners: with a best-effort transport and independently restarting nodes, a single event message could be missed forever, but an assertion repeated at 1 Hz reaches any subscriber within a tick — the same “stream as proof” logic as the Offboard heartbeat. Change-only logging protects the log (and the human reading it) from 1 Hz flooding during a persistent condition: one line when the condition appears, one when it clears, is the record an incident review needs. The latched terminal state protects against oscillation and secondary logic re-entering the mission: a battery hovering around the threshold or a fence flicker under GNSS noise would otherwise toggle the system between mission and failsafe repeatedly — once safety has been invoked, the flight is over, and only a human (restarting the mission after landing) resets it.
- The mission manager, on receiving
LAND:battery below land floor, sets the active-controller token tononebefore calling the land service. Why does the order matter, and what could happen with the reverse order?
The behavior nodes stream Offboard setpoints while the token names them. If the manager called the land service first, PX4 would begin the landing — but the still-authorized behavior node would keep streaming setpoints, and an active setpoint stream is precisely the condition for (re-)entering Offboard mode: the mission logic could fight the landing, or flip the vehicle back out of it. Revoking the token first silences every setpoint source (the nodes stop publishing within one message), so when the land command arrives there is no competing claimant to authority. It is the mission-layer version of the previous chapters’ silence discipline: software hands back control by ceasing to prove it wants control — and the manager makes that happen for the nodes, by decree, rather than trusting each to do it itself.
- PX4’s own low-battery failsafe would eventually return and land the aircraft with no companion software at all. State two distinct reasons the running example nonetheless sets its mission thresholds (0.30/0.15) far above the firmware defaults (~0.15/0.07/0.05), and connect the pattern to a rule from the Airspace Law chapter.
First, the firmware defaults protect the aircraft, not the mission’s geometry: they know nothing about the 200 m fence radius or the 30 m ceiling, so they cannot guarantee the worst-case trip home is affordable when they fire — at 7% remaining, the ~12% return trip is already unpayable. The mission thresholds are sized against the mission’s own worst case, with margin. Second, acting early converts an emergency into a routine: an RTL at 30% is a calm, predictable end-of-mission with the crew watching; an emergency landing at 5% happens wherever the vehicle is, on a sagging pack. The pattern is the layered-margin doctrine of the Airspace Law chapter — the organization’s 10-foot ceiling under the FAA’s 400-foot (122 m) limit — recurring in software: the platform’s legal or firmware floor is where an operation’s margins begin, and every responsible layer adds rules matched to its own hazard profile above it.
12.8 Try It
Part 1 — arm the fence and break it on purpose. Reload the previous chapter’s SITL workbench and the lawnmower mission, and this time finish the job: in QGroundControl, draw an inclusion polygon around the field (or reuse the plan’s), set the geofence breach action to Return-to-Launch, and upload. Fly the mission once inside the fence to confirm nothing fires. Then edit one waypoint to sit outside the polygon and fly again: watch the mission proceed until the boundary, the fence trigger, and the RTL climb-return-land sequence — the mission plan, the vehicle’s track, and the moment of the trigger are the demonstration that matters, so record the screen. Repeat with the action set to Hold and note the difference in what you would do next as an operator: the returning aircraft solved its own problem; the hovering one is holding a draining battery and waiting for you. Finally, lower the fence’s altitude ceiling below the mission altitude and confirm the vertical half of the volume triggers exactly like the horizontal half.
Part 2 — fill the watchdog’s seam. Implement _evaluate in the running example’s failsafe_monitor.py (or a copy of its scaffold) with the four parameterized conditions from this chapter — land-floor battery, fence radius, altitude ceiling, RTL-threshold battery, in that severity order — and test it the way the previous chapter tested the state machine: feed it scripted telemetry rather than a live vehicle. Drive remaining down through 0.30 and 0.15 and check the published severities and the single logged line per transition; walk the position out through 200 m and confirm RTL:geofence; hold the battery at exactly 0.299 and watch the 1 Hz re-publish. Then add the two refinements the chapter argued for: a soft layer (publish WARN: at radius minus a margin — and decide, in writing, who should consume it), and a debounce (a condition must hold for \(k\) consecutive ticks before it is published; pick \(k\) by reasoning about GNSS scatter at the fence line versus the cost of a one-second delay). Finish by wiring your monitor to the SITL vehicle from Part 1 and forcing one real trigger — the moment the simulated aircraft turns for home because your code said so is the moment this Part has been building toward.
Related Lab: See Appendix B for the lab assignment that applies this chapter’s concepts.