AVL & Resource Tracking Feeds

A division supervisor looks at the operating picture and sees eleven engines on the north flank. Nine are reporting normally, one is parked at a water point, and one stopped reporting twenty-two minutes ago on the far side of a ridge. All eleven render as identical markers, because the display shows every unit at its last known position and nothing distinguishes a unit that is stationary from one that has stopped speaking.

Problem Framing

Automatic Vehicle Location (AVL) feeds look like the simplest layer in an incident system — a position, a unit identifier, a timestamp — and they carry more design decisions per byte than anything else on this site. The reason is that a position is not what the operating picture is actually asking for.

What an AVL position is actually asked to answer Four questions the common operating picture asks of a vehicle position, with the accuracy each needs. Is this unit committed or available needs no position at all, only a status; it is answered by the record, not the fix. Which division is it working in needs about 500 metres and is answered by any fix. Can it reach this address in under eight minutes needs about 100 metres and the road segment, because the answer depends on which side of a median the unit is on. Is it inside the hazard perimeter needs about 20 metres and is the only question where a jittering position produces a materially wrong answer. Designing the feed for the fourth question when the first three dominate usage is how AVL pipelines end up expensive and no more useful. most AVL questions do not need a precise position accuracy needed is this unit committed or available? answered by the status field — the position is irrelevant none which division is it working in? any fix answers this, including a bad one ~500 m can it reach this address in under eight minutes? needs the road segment — which side of the median decides the answer ~100 m is it inside the hazard perimeter? the only question where jitter produces a materially wrong answer ~20 m

Only one of those four questions needs a precise position, and it is the least frequently asked. Most AVL usage is answered by the status field and a coarse location, which is why an architecture optimised for positional accuracy tends to be expensive and no more useful. What the picture needs, in descending order of frequency, is: is this unit available, roughly where is it, can it get somewhere, and only occasionally, is it inside a hazard.

The second problem is that AVL is the highest-volume feed in the response, and almost all of that volume carries no information.

Prerequisites

  • A settled unit identifier scheme across every participating agency, or a reconciliation layer that produces one. Two agencies both running an “Engine 11” is the normal case, not an edge case.
  • A message transport with per-message priority, as covered in WebSocket and MQTT for live incident feeds — an AVL flood must not delay a status change.
  • Known radio coverage geometry for the incident area, even approximate. Without it, a silent unit and a unit in a dead zone are indistinguishable, and the display has to treat both as failures.
  • A CRS and axis-order contract per the coordinate reference system standard. AVL vendors are a common source of latitude-first payloads.

Reporting Policy

Message volume from one AVL fleet, by reporting policy A 240-unit fleet reporting for a twelve-hour operational period. A fixed five-second interval produces about 2.07 million messages, most of them from parked units repeating the same position. A fixed thirty-second interval produces about 345,000 but loses the resolution needed to tell which side of a junction a moving unit took. Reporting on movement — a fix only when the unit has moved more than 25 metres or changed status — produces about 210,000, concentrated entirely on units that are actually moving. Adding a heartbeat every two minutes for stationary units brings it to about 245,000 and restores the ability to distinguish a parked unit from a dead radio. The last policy carries more useful information than the first at an eighth of the volume. 240 units, one 12-hour operational period every 5 s, fixed every 30 s, fixed on movement > 25 m movement + 2 min heartbeat 2 070 000 345 000210 000245 000 mostly parked units repeating themselves loses which side of a junction a unit took cannot distinguish parked from dead radio more useful information than the first, at an eighth of the volume Interval-based reporting spends its budget on the units that are not doing anything.

The volume difference is not primarily a bandwidth argument, though on a saturated incident link it is that too. It is that interval-based reporting spends its entire budget on units that are not doing anything, and then has nothing left when thirty units start moving at once — which is exactly the moment the picture matters.

Movement-triggered reporting inverts that. A parked engine costs one message every two minutes; an engine driving a division boundary costs a message every 25 metres. The volume follows the information, and the heartbeat is what preserves the ability to say that a quiet unit is quiet on purpose.

Distinguishing Silence

Three states a unit can be in, and how a feed distinguishes them A unit that has not reported recently is in one of three states and the common operating picture must not conflate them. It may be stationary and healthy, which a heartbeat confirms. It may be moving through a radio dead zone, which is inferred from its last heading and speed plus the known coverage gap, and should be shown as a projected position with growing uncertainty rather than as a stale point. Or its radio may have failed, which is what remains when a heartbeat is missed outside any known coverage gap. Showing all three as a marker at the last known position, which is the default behaviour of most displays, means a supervisor cannot tell a parked engine from one that has stopped reporting. a unit that has not reported is in one of three states — never show them the same way stationary, healthy moving, in a dead zone radio failed confirmed by heartbeat inferred from last heading, speed and known coverage gap heartbeat missed outside any known gap show: solid marker show: projected position with a growing uncertainty ring show: last known, flagged and escalate — this is a safety event age is short and expected age is expected to grow age is unexplained Most displays render all three as a marker at the last known position, so a supervisor cannot tell a parked engine from one whose radio died twenty minutes ago on the fire side of a ridge.

The three states in that figure are the whole reason a heartbeat exists. Without one, “no message for four minutes” is ambiguous between healthy and failed, and a display that resolves the ambiguity by showing the last known position resolves it in the most dangerous direction — a unit whose radio died on the fire side of a ridge appears identical to one at a water point.

A projected position for the dead-zone case is worth the implementation cost. A unit that entered a known gap at 40 km/h on a known heading has a small and computable set of places it can be, and showing that as an expanding uncertainty ring is far more useful to a supervisor than a stale point. Crucially it also degrades honestly: the ring grows until it covers the whole gap, at which point the display is saying “somewhere in here”, which is true.

Step-by-Step Implementation

python
from __future__ import annotations

import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import Enum

from shapely.geometry import Point
from shapely.geometry.base import BaseGeometry

logger = logging.getLogger("incidentgis.avl")

MOVEMENT_THRESHOLD_M = 25.0
HEARTBEAT_INTERVAL = timedelta(minutes=2)
# How long past a missed heartbeat before a unit is treated as a safety event.
SILENCE_ESCALATION = timedelta(minutes=5)


class UnitState(str, Enum):
    REPORTING = "reporting"
    STATIONARY = "stationary"
    PROJECTED = "projected"       # in a known coverage gap
    SILENT = "silent"             # unexplained — escalate


@dataclass(frozen=True)
class AvlFix:
    unit_id: str
    position: Point               # in the incident's projected CRS
    heading_deg: float
    speed_mps: float
    status: str
    reported_at: datetime


def should_transmit(previous: AvlFix | None, current: AvlFix) -> bool:
    """Movement-triggered reporting with a heartbeat.

    Volume follows information: a parked unit costs one message per heartbeat
    interval, a moving one costs a message per threshold of travel.
    """
    if previous is None:
        return True
    if current.status != previous.status:
        return True                      # status changes are never suppressed
    if current.position.distance(previous.position) >= MOVEMENT_THRESHOLD_M:
        return True
    return current.reported_at - previous.reported_at >= HEARTBEAT_INTERVAL


def classify_silence(
    last: AvlFix,
    now: datetime,
    coverage_gaps: BaseGeometry,
) -> tuple[UnitState, BaseGeometry | None]:
    """Decide what a unit's silence means, and what to draw.

    Returns the state and, for a projected unit, the region it could be in.
    Never returns a bare last-known point for a unit that should be escalated.
    """
    age = now - last.reported_at
    if age < HEARTBEAT_INTERVAL * 1.5:
        return (UnitState.STATIONARY if last.speed_mps < 1.0
                else UnitState.REPORTING), None

    in_gap = coverage_gaps.contains(last.position) or coverage_gaps.distance(
        last.position
    ) < last.speed_mps * age.total_seconds()

    if in_gap:
        # The unit can only be somewhere within its travel radius, intersected
        # with the gap. The ring grows honestly until it covers the whole gap.
        reach_m = max(last.speed_mps, 1.0) * age.total_seconds()
        possible = last.position.buffer(reach_m).intersection(coverage_gaps)
        logger.info("avl_unit_projected", extra={
            "unit_id": last.unit_id, "age_s": int(age.total_seconds()),
            "reach_m": round(reach_m),
        })
        return UnitState.PROJECTED, possible

    if age >= SILENCE_ESCALATION:
        # Not in a known gap and past the escalation threshold: this is a
        # safety event, not a display quirk.
        logger.warning("avl_unit_silent", extra={
            "unit_id": last.unit_id, "age_s": int(age.total_seconds()),
            "last_status": last.status,
        })
        return UnitState.SILENT, None

    return UnitState.STATIONARY, None

Configuration Reference

Parameter Env var Default Notes
Movement threshold AVL_MOVEMENT_M 25 Below the width of most road corridors, so a lane change does not report.
Heartbeat interval AVL_HEARTBEAT_S 120 The upper bound on how long a healthy quiet unit looks unexplained.
Silence escalation AVL_SILENCE_S 300 Past this, outside a known gap, it is a safety event.
Coverage gap layer AVL_COVERAGE_GAPS unset Without it every dead-zone unit escalates and the alert becomes noise.
Status priority topic AVL_STATUS_TOPIC unit/+/status Status changes ride a separate topic so an AVL flood cannot delay them.
Position CRS AVL_CRS incident UTM Distances are metric; a geographic CRS makes the threshold meaningless.
Max projected radius AVL_MAX_PROJECT_M 3000 Beyond this the projection says nothing useful; fall back to escalation.

Verification and Smoke Test

python
# A parked unit must produce heartbeats and nothing else.
assert sum(should_transmit(prev, fix) for prev, fix in parked_sequence) == expected_heartbeats

# A unit crossing a known dead zone must be PROJECTED, never SILENT.
state, region = classify_silence(last_fix, now, coverage_gaps)
assert state is UnitState.PROJECTED and region is not None

# A unit silent in good coverage must escalate.
state, _ = classify_silence(open_air_fix, now, coverage_gaps)
assert state is UnitState.SILENT

Integration With Adjacent Workflows

Unit positions feed the evacuation routing layer when computing which units can reach an address, and a projected position must be marked so the router does not treat it as surveyed. Status changes are validated by the same attribute validation rules as any other record, and AVL positions are subject to the same null-island and bounds checks — vendors are a common source of out-of-area fixes.

One organisational note that outlasts any of the parameters above. The reporting policy is a negotiation with the agencies whose vehicles are being tracked, not purely an engineering choice — a movement threshold that is comfortable for a fire district may be unacceptably coarse to a law-enforcement partner whose units are tracked for officer safety rather than for resource allocation. Agree the policy per agency, record it alongside the feed configuration, and expect it to differ across the participating organisations rather than trying to impose one number on all of them. A feed whose behaviour is documented and different is far easier to reason about during an incident than one that is uniform and quietly wrong for half its contributors.

Troubleshooting

Symptom: the message rate collapses when the incident gets busy. Movement-triggered reporting is working as designed and the transport is saturating. Shed by widening the movement threshold for available units, never for committed ones.

Symptom: every unit in a canyon escalates as silent. The coverage gap layer is missing or too coarse. Without it, dead zones are indistinguishable from failures and the alert becomes noise that gets muted.

Symptom: units jump between two positions while parked. Multipath at a fixed location, covered in handling GPS drift in urban canyons. Raising the movement threshold hides it; fixing it means detecting the directional bias.

Symptom: two agencies’ units overwrite each other. Unit identifiers collide across agencies. The identifier must be scoped by agency before it reaches the picture.

Symptom: positions are in the ocean. Latitude-first payload from the AVL vendor. Assert bounds at ingest rather than trusting the feed.

Frequently Asked Questions

Does an AVL feed need high positional accuracy? Rarely. Of the four questions the operating picture asks of a vehicle position, the most common — is this unit committed or available — is answered by the status field and needs no position at all, and which division it is working in is answered by any fix. Reaching an address in a given time needs roughly 100 metres and the road segment, because which side of a median a unit is on decides the answer. Only containment inside a hazard perimeter needs about 20 metres, and it is the least frequently asked. An architecture optimised for the rarest question tends to be expensive without being more useful.

Why report on movement instead of on a fixed interval? Because interval reporting spends its budget on units that are not doing anything. A 240-unit fleet reporting every five seconds produces about 2.07 million messages over a twelve-hour period, most of them parked units repeating the same position, and has nothing left when thirty units start moving at once. Reporting when a unit has moved more than about 25 metres or changed status produces roughly 210,000 messages concentrated on units that are actually moving, and adding a two-minute heartbeat brings it to about 245,000 — more useful information than the five-second policy at an eighth of the volume.

How should a display handle a unit that has stopped reporting? By distinguishing three states rather than showing a marker at the last known position. A stationary healthy unit is confirmed by its heartbeat and can be drawn solid. A unit inside a known radio coverage gap should be drawn as a projected region derived from its last heading and speed, growing until it covers the gap, which is an honest statement of what is known. A unit silent outside any known gap past an escalation threshold is a safety event and should be flagged and escalated. Rendering all three identically means a supervisor cannot tell a parked engine from one whose radio died on the fire side of a ridge.

Up: Incident Mapping & Multi-Agency Sync Workflows

Continue inside this section

Other guides in Incident Mapping & Multi-Agency Sync Workflows: Production Architecture for Python Emergency Response