Skip to content

Proxy Handlers

Handlers for the BlueSky data events the proxy subscribes to (see WebATM.proxy.subscribers for the topic → handler mapping).

WebATM.proxy.handlers.simulation

WebATM.proxy.handlers.simulation

Handle BlueSky simulation-state events (SIMINFO, ACDATA, STATECHANGE).

These handlers receive the core simulation feed from the BlueSky server: the per-node simulation clock and state (SIMINFO), the aircraft traffic frames (ACDATA), and explicit run-state transitions (STATECHANGE). They cache the data on the proxy and forward it to connected browsers over Socket.IO as the siminfo, acdata, and statechange events.

on_siminfo_received

on_siminfo_received(
    speed,
    simdt,
    simt,
    simutc,
    ntraf,
    state,
    scenname,
    sender_id=None,
)

Process a BlueSky SIMINFO event and emit siminfo to web clients.

Updates the per-node status/clock tracking for every sending node (feeding the Simulation Nodes panel via a throttled node_info emission), but only caches and emits the header simulation info for the active node so the displayed clock does not jump between nodes. Emissions to browsers are throttled to proxy.siminfo_interval.

Parameters:

Name Type Description Default
speed float

Simulation speed multiplier.

required
simdt float

Simulation timestep in seconds.

required
simt float

Elapsed simulation time in seconds.

required
simutc str

Simulation UTC time string.

required
ntraf int

Number of aircraft currently in the simulation.

required
state int

Simulation run-state code.

required
scenname str

Name of the currently loaded scenario.

required
sender_id bytes | str | None

Identifier of the sending node; bytes are converted to a hex string.

None
Source code in WebATM/proxy/handlers/simulation.py
def on_siminfo_received(
    speed, simdt, simt, simutc, ntraf, state, scenname, sender_id=None
):
    """Process a BlueSky SIMINFO event and emit ``siminfo`` to web clients.

    Updates the per-node status/clock tracking for every sending node (feeding
    the Simulation Nodes panel via a throttled ``node_info`` emission), but only
    caches and emits the header simulation info for the active node so the
    displayed clock does not jump between nodes. Emissions to browsers are
    throttled to ``proxy.siminfo_interval``.

    Args:
        speed (float): Simulation speed multiplier.
        simdt (float): Simulation timestep in seconds.
        simt (float): Elapsed simulation time in seconds.
        simutc (str): Simulation UTC time string.
        ntraf (int): Number of aircraft currently in the simulation.
        state (int): Simulation run-state code.
        scenname (str): Name of the currently loaded scenario.
        sender_id (bytes | str | None): Identifier of the sending node; bytes
            are converted to a hex string.
    """
    proxy = active_proxy()
    if not proxy:
        return

    sender_id_str = id2str(sender_id)
    current_time = time.time()

    sim_data = {
        "speed": float(speed) if speed is not None else 0.0,
        "simdt": float(simdt) if simdt is not None else 0.0,
        "simt": float(simt) if simt is not None else 0.0,
        "simutc": str(simutc) if simutc is not None else "",  # UTC is a string
        "ntraf": int(ntraf) if ntraf is not None else 0,
        "state": int(state) if state is not None else 0,
        "scenname": str(scenname) if scenname is not None else "",
        "sender_id": sender_id_str,  # Include sender ID for node identification
    }

    # Update per-node tracking for EVERY node so the Simulation Nodes panel
    # shows each node's own clock, regardless of which node is currently active.
    if sender_id_str and sender_id_str in proxy.tracked_nodes:
        simt_str = tim2txt(simt)[:-3] if simt is not None else "00:00:00"
        proxy.tracked_nodes[sender_id_str].update(
            {"status": scenname or "init", "time": simt_str}
        )
        # Refresh the Nodes panel on a wall-clock cadence, not every frame. A
        # sim-time throttle (int(simt) % 5) misbehaves when the sim is paused
        # (spams or never fires, depending on the frozen value) or fast-forwarded
        # (frames jump past the multiple), so throttle on real time instead.
        if (current_time - proxy.last_node_info_emit) >= proxy.node_info_interval:
            proxy.last_node_info_emit = current_time
            proxy._emit_node_info()

    # Cache and emit the header sim info solely for the active node.
    if not _is_active_node(proxy, sender_id_str):
        return

    proxy.sim_data = sim_data

    # Throttle sim info emissions
    if (
        proxy.socketio
        and proxy.connected_clients > 0
        and (current_time - proxy.last_siminfo_emit) >= proxy.siminfo_interval
    ):
        try:
            proxy.socketio.emit("siminfo", sim_data)
            proxy.last_siminfo_emit = current_time
        except Exception as e:
            logger.error(f"Proxy→Web: Error sending SIMINFO: {e}")

on_acdata_received

on_acdata_received(data)

Process a BlueSky ACDATA traffic frame and emit acdata to web clients.

On a simulation reset or active-node change (detected via the BlueSky network context), clears the cached traffic data and immediately emits an empty acdata payload so browsers drop stale aircraft.

Hot path: the network timer delivers ACDATA at up to 50 Hz, but it is only emitted to browsers at acdata_interval (10 Hz) and only for the active node. make_json_serializable is the dominant per-frame cost, so it is deferred until after the active-node filter and the emit throttle decide the frame is actually sent. Set WEBATM_PERF=1 (WebATM.proxy.perf) to measure it.

Parameters:

Name Type Description Default
data dict

Aircraft state arrays keyed by field (id, lat, lon, alt, tas, trk, vs, conflict counters, ...) as sent by the BlueSky server.

required
Source code in WebATM/proxy/handlers/simulation.py
def on_acdata_received(data):
    """Process a BlueSky ACDATA traffic frame and emit ``acdata`` to web clients.

    On a simulation reset or active-node change (detected via the BlueSky
    network context), clears the cached traffic data and immediately emits an
    empty ``acdata`` payload so browsers drop stale aircraft.

    Hot path: the network timer delivers ACDATA at up to 50 Hz, but it is only
    emitted to browsers at ``acdata_interval`` (10 Hz) and only for the active
    node. ``make_json_serializable`` is the dominant per-frame cost, so it is
    deferred until after the active-node filter and the emit throttle decide the
    frame is actually sent. Set WEBATM_PERF=1 (WebATM.proxy.perf) to measure it.

    Args:
        data (dict): Aircraft state arrays keyed by field (``id``, ``lat``,
            ``lon``, ``alt``, ``tas``, ``trk``, ``vs``, conflict counters, ...)
            as sent by the BlueSky server.
    """
    try:
        proxy = get_bluesky_proxy()
        if not proxy:
            logger.debug("on_acdata_received called but no proxy available")
            return

        # Ignore data if reconnection is not allowed (we're disconnected)
        if not proxy.allow_reconnection:
            logger.debug("on_acdata_received ignored - reconnection not allowed")
            return

        # Check context action like BlueSky web client does, and resolve which
        # node sent this frame (set on the shared context just before this
        # synchronous dispatch) for the active-node filter below.
        sender_id_str = None
        if proxy.bluesky_client and hasattr(proxy.bluesky_client, "context"):
            ctx = proxy.bluesky_client.context
            if ctx.action == ctx.Reset or ctx.action == ctx.ActChange:
                # Simulation reset or active-node change: clear all aircraft.
                logger.info("ACDATA reset/actchange detected - clearing aircraft data")
                cleared = empty_traffic_data()
                proxy.traffic_data = cleared

                # Emit cleared data immediately
                if proxy.socketio and proxy.connected_clients > 0:
                    try:
                        proxy.socketio.emit("acdata", cleared)
                        logger.debug(
                            f"Emitted cleared ACDATA to {proxy.connected_clients} web clients"
                        )
                    except Exception as e:
                        logger.error(f"Error emitting cleared ACDATA: {e}")
                return

            sender_id_str = id2str(getattr(ctx, "sender_id", None))

        # Any ACDATA from any node proves the link to BlueSky is alive: update
        # liveness before filtering so a background node's traffic still counts.
        proxy.last_successful_update = time.time()
        data_path_perf.record_received()

        # Only the active node's traffic is displayed. Skip serializing frames
        # from background nodes nobody is viewing (mirrors the SIMINFO filter).
        if not _is_active_node(proxy, sender_id_str):
            data_path_perf.record_filtered()
            return

        # Throttle BEFORE serializing. Emitting (and therefore serializing) only
        # at acdata_interval removes the wasted per-frame work under heavy node
        # load. traffic_data refreshes at the emit cadence, which is what the
        # initial-data snapshot and the 0.5 s backup emit consume.
        current_time = time.time()
        if not (
            proxy.socketio
            and proxy.connected_clients > 0
            and (current_time - proxy.last_acdata_emit) >= proxy.acdata_interval
        ):
            return

        t0 = time.perf_counter()
        serializable_data = make_json_serializable(data)
        data_path_perf.record_serialize(time.perf_counter() - t0)
        proxy.traffic_data = serializable_data

        try:
            t1 = time.perf_counter()
            proxy.socketio.emit("acdata", serializable_data)
            proxy.last_acdata_emit = current_time
            data_path_perf.record_emit(time.perf_counter() - t1)
        except Exception as e:
            logger.error(f"Error emitting ACDATA: {e}")
            import traceback

            traceback.print_exc()

    except Exception as e:
        logger.error(f"ACDATA Handler: Detailed error in on_acdata_received: {e}")
        logger.error(f"ACDATA Handler: Error type: {type(e).__name__}")
        logger.error(f"ACDATA Handler: Data type: {type(data)}")
        logger.error(f"ACDATA Handler: Data content: {data}")
        import traceback

        traceback.print_exc()
    finally:
        data_path_perf.maybe_log()

on_statechange_received

on_statechange_received(data, sender_id=None)

Process a BlueSky STATECHANGE event and emit statechange to web clients.

Updates the cached simulation state (proxy.sim_data['state']) and immediately forwards the new run-state and sending node to connected browsers, without throttling. Like SIMINFO, only the active node's state changes touch the cached header state — a paused background node must not flip the displayed run-state.

Parameters:

Name Type Description Default
data dict

Event payload; the simstate key holds the new run-state code. Payloads without simstate are ignored.

required
sender_id bytes | str | None

Identifier of the sending node; bytes are converted to a hex string.

None
Source code in WebATM/proxy/handlers/simulation.py
def on_statechange_received(data, sender_id=None):
    """Process a BlueSky STATECHANGE event and emit ``statechange`` to web clients.

    Updates the cached simulation state (``proxy.sim_data['state']``) and
    immediately forwards the new run-state and sending node to connected
    browsers, without throttling. Like SIMINFO, only the active node's state
    changes touch the cached header state — a paused background node must not
    flip the displayed run-state.

    Args:
        data (dict): Event payload; the ``simstate`` key holds the new
            run-state code. Payloads without ``simstate`` are ignored.
        sender_id (bytes | str | None): Identifier of the sending node; bytes
            are converted to a hex string.
    """
    proxy = active_proxy()
    if not proxy:
        return

    simstate = data.get("simstate") if isinstance(data, dict) else None
    if simstate is None:
        return

    sender_id_str = id2str(sender_id)

    if not _is_active_node(proxy, sender_id_str):
        logger.debug(
            f"STATECHANGE from background node {sender_id_str} ignored "
            f"(simstate={simstate})"
        )
        return

    # Only patch an existing SIMINFO cache; seeding a bare {'state': ...} would
    # let the backup timer re-emit a partial siminfo payload.
    if proxy.sim_data:
        proxy.sim_data["state"] = int(simstate)

    payload = {
        "simstate": int(simstate),
        "sender_id": sender_id_str,
    }

    if proxy.socketio and proxy.connected_clients > 0:
        try:
            proxy.socketio.emit("statechange", payload)
        except Exception as e:
            logger.error(f"Proxy->Web: Error sending STATECHANGE: {e}")

    logger.info(f"STATECHANGE from {sender_id_str}: simstate={simstate}")

WebATM.proxy.handlers.shapes

WebATM.proxy.handlers.shapes

Handle BlueSky POLY shape events and split them into polygons and polylines.

BlueSky publishes all drawn shapes on a single POLY topic as shared-state updates: the network client strips the [action, payload] wrapper and records the action (Update/Delete/Replace/...) on its context before dispatching here. This module applies each action to the per-node shape stores on the proxy, separates polygons from polylines by their shape field, and forwards the active node's shapes to browsers as the poly and polyline Socket.IO events.

on_poly_received

on_poly_received(data, *args, **kwargs)

Process a BlueSky POLY event and emit poly/polyline to web clients.

Resolves the sending node and shared-state action from the BlueSky network context and applies the message to that node's stored shapes: Delete removes the named shapes, Replace/Reset/ActChange overwrite the stored sets, and updates merge into them (patching partial per-shape updates such as a colour change). At most the five most recent polygons and polylines are kept per node. The complete stored shape sets are emitted only when the sender is the currently active node.

Parameters:

Name Type Description Default
data dict

POLY payload from the BlueSky server (the shared-state action wrapper is already stripped by the network client): a polys mapping of shape name to shape info, or a list of shape names for a Delete action.

required
*args Any

Extra positional arguments from the network dispatch (unused).

()
**kwargs Any

Extra keyword arguments from the network dispatch (unused).

{}
Source code in WebATM/proxy/handlers/shapes.py
def on_poly_received(data, *args, **kwargs):
    """Process a BlueSky POLY event and emit ``poly``/``polyline`` to web clients.

    Resolves the sending node and shared-state action from the BlueSky network
    context and applies the message to that node's stored shapes: Delete
    removes the named shapes, Replace/Reset/ActChange overwrite the stored
    sets, and updates merge into them (patching partial per-shape updates such
    as a colour change). At most the five most recent polygons and polylines
    are kept per node. The complete stored shape sets are emitted only when
    the sender is the currently active node.

    Args:
        data (dict): POLY payload from the BlueSky server (the shared-state
            action wrapper is already stripped by the network client): a
            ``polys`` mapping of shape name to shape info, or a list of shape
            names for a Delete action.
        *args (Any): Extra positional arguments from the network dispatch (unused).
        **kwargs (Any): Extra keyword arguments from the network dispatch (unused).
    """
    proxy = active_proxy()
    if not proxy:
        return

    try:
        sender_id, action = _context_info(proxy)
        poly_data = make_json_serializable(data)

        if sender_id:
            poly_store = proxy.poly_data_by_node.setdefault(sender_id, {"polys": {}})
            line_store = proxy.polyline_data_by_node.setdefault(
                sender_id, {"polys": {}}
            )

            if action == _DELETE_ACTION:
                for name in _deleted_names(poly_data):
                    poly_store["polys"].pop(name, None)
                    line_store["polys"].pop(name, None)
            else:
                separated = _separate_poly_and_polyline_data(poly_data)
                new_polys = _shapes_of(separated["polygons"])
                new_lines = _shapes_of(separated["polylines"])

                if action in _REPLACE_ACTIONS:
                    poly_store["polys"] = new_polys
                    line_store["polys"] = new_lines
                else:
                    _merge_shapes(
                        poly_store["polys"], line_store["polys"], new_polys, new_lines
                    )

                _trim_shape_store(poly_store["polys"], "polygons")
                _trim_shape_store(line_store["polys"], "polylines")

        # Only the active node's shapes are displayed; emit the complete stored
        # sets (not just this message's shapes).
        active_node_id = proxy._get_safe_active_node()
        if sender_id and active_node_id and sender_id == active_node_id:
            if proxy.socketio:
                proxy.socketio.emit(
                    "poly", proxy.poly_data_by_node.get(sender_id, {"polys": {}})
                )
                proxy.socketio.emit(
                    "polyline",
                    proxy.polyline_data_by_node.get(sender_id, {"polys": {}}),
                )

    except Exception as e:
        logger.error(f"Error processing POLY data: {e}")
        import traceback

        traceback.print_exc()

WebATM.proxy.handlers.commands

WebATM.proxy.handlers.commands

Handle BlueSky stack-command events (STACK, STACKCMDS).

STACKCMDS carries the server's command dictionary, which is cached on the proxy and forwarded to browsers as the cmddict Socket.IO event so the web console can validate and autocomplete commands. STACK carries command lines forwarded by the server — commands the simulation did not recognize and assumes are GUI/client commands (typically PAN/ZOOM lines in scenario files). They are handled locally and never echoed back to the server.

on_stackcmds_received

on_stackcmds_received(action, data)

Process a BlueSky STACKCMDS event and emit cmddict to web clients.

When the payload is a dict, merges its cmddict mapping into the proxy's command dictionary and emits the updated dictionary to connected browsers. Other payload shapes are only logged.

Parameters:

Name Type Description Default
action Any

Action marker delivered with the event (unused).

required
data dict | bytes | str

STACKCMDS payload; a dict is expected to contain a cmddict mapping of command names to metadata.

required
Source code in WebATM/proxy/handlers/commands.py
def on_stackcmds_received(action, data):
    """Process a BlueSky STACKCMDS event and emit ``cmddict`` to web clients.

    When the payload is a dict, merges its ``cmddict`` mapping into the proxy's
    command dictionary and emits the updated dictionary to connected browsers.
    Other payload shapes are only logged.

    Args:
        action (Any): Action marker delivered with the event (unused).
        data (dict | bytes | str): STACKCMDS payload; a dict is expected to
            contain a ``cmddict`` mapping of command names to metadata.
    """
    proxy = active_proxy()
    if not proxy:
        return

    if not isinstance(data, dict):
        logger.debug(f"Ignoring non-dict STACKCMDS payload of type {type(data)}")
        return

    cmddict = data.get("cmddict")
    if not isinstance(cmddict, dict):
        logger.warning(f"STACKCMDS payload without a cmddict mapping: {data.keys()}")
        return

    proxy.cmddict.update(cmddict)
    logger.debug(f"Updated cmddict with {len(cmddict)} commands")

    if proxy.socketio and proxy.connected_clients > 0:
        try:
            proxy.socketio.emit("cmddict", {"cmddict": proxy.cmddict})
        except Exception as e:
            logger.error(f"Error emitting cmddict: {e}")

on_stack_received

on_stack_received(data)

Process a BlueSky STACK event carrying server-forwarded command lines.

Normalizes the payload to a list of command lines and runs each non-empty line through _process_server_command, which executes it locally and reports the outcome via the proxy's echo channel. Nothing is sent back to the BlueSky server.

Parameters:

Name Type Description Default
data str | list | tuple

One command line, or a sequence of command lines, forwarded by the server. Other types are logged and ignored.

required
Source code in WebATM/proxy/handlers/commands.py
def on_stack_received(data):
    """Process a BlueSky STACK event carrying server-forwarded command lines.

    Normalizes the payload to a list of command lines and runs each non-empty
    line through ``_process_server_command``, which executes it locally and
    reports the outcome via the proxy's echo channel. Nothing is sent back to
    the BlueSky server.

    Args:
        data (str | list | tuple): One command line, or a sequence of command
            lines, forwarded by the server. Other types are logged and ignored.
    """
    proxy = active_proxy()
    if not proxy:
        return

    if isinstance(data, str):
        commands_to_process = [data]
    elif isinstance(data, (list, tuple)):
        commands_to_process = list(data)
    else:
        logger.warning(f"Unexpected STACK data format: {type(data)}")
        return

    for cmdline in commands_to_process:
        if isinstance(cmdline, str) and cmdline.strip():
            _process_server_command(proxy, cmdline)

WebATM.proxy.handlers.echo

WebATM.proxy.handlers.echo

Echo message handler for command responses.

echo

echo(text, flags=None, sender_id=None)

Handle ECHO messages (command responses) from the simulation.

Stores the message on the proxy and emits an echo event to connected web clients immediately — command responses are never throttled.

Parameters:

Name Type Description Default
text str

The echo text; newlines and formatting are preserved.

required
flags int | None

BlueSky echo flags (e.g. error indication). Defaults to 0 when None.

None
sender_id bytes | str | None

ID of the node that sent the echo; decoded to a readable string for the client.

None
Source code in WebATM/proxy/handlers/echo.py
def echo(text, flags=None, sender_id=None):
    """Handle ECHO messages (command responses) from the simulation.

    Stores the message on the proxy and emits an ``echo`` event to connected
    web clients immediately — command responses are never throttled.

    Args:
        text (str): The echo text; newlines and formatting are preserved.
        flags (int | None): BlueSky echo flags (e.g. error indication).
            Defaults to 0 when None.
        sender_id (bytes | str | None): ID of the node that sent the echo;
            decoded to a readable string for the client.
    """
    proxy = active_proxy()
    if not proxy:
        return

    # Preserve newlines and formatting in the echo data
    formatted_text = str(text) if text is not None else ""

    # Decode sender_id from bytes to readable string
    sender_str = None
    if sender_id is not None:
        if isinstance(sender_id, bytes):
            sender_str = safe_decode(sender_id)
        else:
            sender_str = str(sender_id)

    echo_data = {
        "text": formatted_text,  # Keep original formatting including \n
        "flags": int(flags) if flags is not None else 0,
        "timestamp": time.time(),
        "sender": sender_str,
    }
    proxy.echo_data = echo_data

    # Send echo messages immediately (no throttling for command responses)
    if proxy.socketio and proxy.connected_clients > 0:
        try:
            proxy.socketio.emit("echo", echo_data)
        except Exception as e:
            logger.error(f"Error sending echo to web client: {e}")

WebATM.proxy.handlers.routes

WebATM.proxy.handlers.routes

Route data handler for aircraft route visualization.

on_routedata_received

on_routedata_received(data)

Handle ROUTEDATA events carrying an aircraft's route.

Serializes the route and emits a routedata event to connected web clients. Frames that carry waypoints are only forwarded when the aircraft is part of the active node's traffic, preventing flicker when switching between nodes. Waypoint-less frames are BlueSky's route-clear broadcasts (route display toggled off, or the aircraft was deleted) and are always forwarded — the aircraft they refer to may already be gone from the traffic, and clients need them to drop their cached route.

Parameters:

Name Type Description Default
data dict

ROUTEDATA payload with the aircraft ID (acid) and, for route updates, waypoint arrays (wplat, wplon, ...).

required
Source code in WebATM/proxy/handlers/routes.py
def on_routedata_received(data):
    """Handle ROUTEDATA events carrying an aircraft's route.

    Serializes the route and emits a ``routedata`` event to connected web
    clients. Frames that carry waypoints are only forwarded when the aircraft
    is part of the active node's traffic, preventing flicker when switching
    between nodes. Waypoint-less frames are BlueSky's route-clear broadcasts
    (route display toggled off, or the aircraft was deleted) and are always
    forwarded — the aircraft they refer to may already be gone from the
    traffic, and clients need them to drop their cached route.

    Args:
        data (dict): ROUTEDATA payload with the aircraft ID (``acid``) and,
            for route updates, waypoint arrays (``wplat``, ``wplon``, ...).
    """
    proxy = active_proxy()
    if not proxy:
        return

    if not proxy._get_safe_active_node():
        logger.debug("Route data ignored - no active node available")
        return

    route_aircraft_id = data.get("acid") if isinstance(data, dict) else None
    if not route_aircraft_id:
        logger.debug("Route data ignored - no aircraft ID found")
        return

    wplat = data.get("wplat")
    has_waypoints = wplat is not None and len(wplat) > 0
    if has_waypoints and proxy.traffic_data:
        if route_aircraft_id not in proxy.traffic_data.get("id", []):
            return

    route_data = make_json_serializable(data)

    if proxy.socketio and proxy.connected_clients > 0:
        try:
            proxy.socketio.emit("routedata", route_data)
        except Exception:
            # Emission errors (e.g. disconnected clients) are non-fatal
            pass

WebATM.proxy.handlers.events

WebATM.proxy.handlers.events

Event handlers for RESET and REQUEST events.

on_reset_received

on_reset_received(
    data=None, *args, sender_id=None, **kwargs
)

Handle RESET events from the BlueSky server.

Clears the stored polygon/polyline shapes for the node that sent the reset. Browsers display the active node only, so the map-clearing poly and polyline payloads and the reset event are emitted solely when the resetting node is the active one — a background node's reset must not wipe the active node's display. When the sender or active node can't be resolved, the reset is accepted so a single-node display still works (same fallback as the SIMINFO/ACDATA active-node filter).

Parameters:

Name Type Description Default
data Any

Optional RESET payload (unused).

None
*args Any

Additional positional payload items (unused).

()
sender_id bytes | str | None

Node that reset, from the message header; bytes are converted to a hex string. The shared network context is deliberately not consulted — it holds the sender of the last shared-state message (usually the active node), not of this RESET.

None
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/events.py
def on_reset_received(data=None, *args, sender_id=None, **kwargs):
    """Handle RESET events from the BlueSky server.

    Clears the stored polygon/polyline shapes for the node that sent the
    reset. Browsers display the active node only, so the map-clearing ``poly``
    and ``polyline`` payloads and the ``reset`` event are emitted solely when
    the resetting node is the active one — a background node's reset must not
    wipe the active node's display. When the sender or active node can't be
    resolved, the reset is accepted so a single-node display still works
    (same fallback as the SIMINFO/ACDATA active-node filter).

    Args:
        data (Any): Optional RESET payload (unused).
        *args (Any): Additional positional payload items (unused).
        sender_id (bytes | str | None): Node that reset, from the message
            header; bytes are converted to a hex string. The shared network
            context is deliberately not consulted — it holds the sender of the
            last shared-state message (usually the active node), not of this
            RESET.
        **kwargs (Any): Additional keyword payload items (unused).
    """
    proxy = active_proxy()
    if not proxy:
        return

    try:
        sender_id = id2str(sender_id)

        active_node_id = proxy._get_safe_active_node()
        reset_node_id = sender_id or active_node_id
        if not reset_node_id:
            return

        # Only the resetting node's stored shapes are stale.
        proxy.poly_data_by_node.pop(reset_node_id, None)
        proxy.polyline_data_by_node.pop(reset_node_id, None)

        is_active_node = (
            active_node_id is None or sender_id is None or sender_id == active_node_id
        )
        if is_active_node and proxy.socketio and proxy.connected_clients > 0:
            proxy.socketio.emit("poly", {"polys": {}})
            proxy.socketio.emit("polyline", {"polys": {}})
            proxy.socketio.emit(
                "reset",
                {"reason": "BlueSky simulation reset", "timestamp": time.time()},
            )

    except Exception as e:
        logger.error(f"Error processing RESET data: {e}")

on_request_received

on_request_received(data, *args, **kwargs)

Handle REQUEST events from the BlueSky server.

Currently only logs the payload; specific request handling is not yet implemented.

Parameters:

Name Type Description Default
data Any

The REQUEST payload.

required
*args Any

Additional positional payload items (unused).

()
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/events.py
def on_request_received(data, *args, **kwargs):
    """Handle REQUEST events from the BlueSky server.

    Currently only logs the payload; specific request handling is not yet
    implemented.

    Args:
        data (Any): The REQUEST payload.
        *args (Any): Additional positional payload items (unused).
        **kwargs (Any): Additional keyword payload items (unused).
    """
    if not active_proxy():
        return

    # TODO: Implement specific request handling logic
    logger.debug(f"REQUEST data received: {data}")

WebATM.proxy.handlers.visualization

WebATM.proxy.handlers.visualization

Visualization handlers for PLOT, TRAILS, SHOWDIALOG, and SIMSETTINGS events.

on_plot_received

on_plot_received(data, *args, **kwargs)

Handle PLOT events from the BlueSky server.

Currently only logs the payload; plot visualization is not yet implemented in the web client.

Parameters:

Name Type Description Default
data Any

The PLOT payload.

required
*args Any

Additional positional payload items (unused).

()
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/visualization.py
def on_plot_received(data, *args, **kwargs):
    """Handle PLOT events from the BlueSky server.

    Currently only logs the payload; plot visualization is not yet
    implemented in the web client.

    Args:
        data (Any): The PLOT payload.
        *args (Any): Additional positional payload items (unused).
        **kwargs (Any): Additional keyword payload items (unused).
    """
    if not active_proxy():
        return

    try:
        logger.debug(f"PLOT data received: {data}")
        # TODO: Implement plot data handling and visualization
    except Exception as e:
        logger.error(f"Error processing PLOT data: {e}")

on_showdialog_received

on_showdialog_received(data, *args, **kwargs)

Handle SHOWDIALOG events from the BlueSky server.

Currently only logs the payload; dialog display in the web interface is not yet implemented.

Parameters:

Name Type Description Default
data Any

The SHOWDIALOG payload.

required
*args Any

Additional positional payload items (unused).

()
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/visualization.py
def on_showdialog_received(data, *args, **kwargs):
    """Handle SHOWDIALOG events from the BlueSky server.

    Currently only logs the payload; dialog display in the web interface is
    not yet implemented.

    Args:
        data (Any): The SHOWDIALOG payload.
        *args (Any): Additional positional payload items (unused).
        **kwargs (Any): Additional keyword payload items (unused).
    """
    if not active_proxy():
        return

    try:
        logger.debug(f"SHOWDIALOG data received: {data}")
        # TODO: Implement dialog display logic for web interface
    except Exception as e:
        logger.error(f"Error processing SHOWDIALOG data: {e}")

on_simsettings_received

on_simsettings_received(data, *args, **kwargs)

Handle SIMSETTINGS events from the BlueSky server.

Currently only logs the payload; simulation settings handling is not yet implemented.

Parameters:

Name Type Description Default
data Any

The SIMSETTINGS payload.

required
*args Any

Additional positional payload items (unused).

()
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/visualization.py
def on_simsettings_received(data, *args, **kwargs):
    """Handle SIMSETTINGS events from the BlueSky server.

    Currently only logs the payload; simulation settings handling is not yet
    implemented.

    Args:
        data (Any): The SIMSETTINGS payload.
        *args (Any): Additional positional payload items (unused).
        **kwargs (Any): Additional keyword payload items (unused).
    """
    if not active_proxy():
        return

    try:
        logger.debug(f"SIMSETTINGS data received: {data}")
        # TODO: Implement simulation settings handling
    except Exception as e:
        logger.error(f"Error processing SIMSETTINGS data: {e}")

on_trails_received

on_trails_received(data, *args, **kwargs)

Handle TRAILS events from the BlueSky server.

Currently only logs the payload; aircraft trail visualization is not yet implemented.

Parameters:

Name Type Description Default
data Any

The TRAILS payload.

required
*args Any

Additional positional payload items (unused).

()
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/visualization.py
def on_trails_received(data, *args, **kwargs):
    """Handle TRAILS events from the BlueSky server.

    Currently only logs the payload; aircraft trail visualization is not yet
    implemented.

    Args:
        data (Any): The TRAILS payload.
        *args (Any): Additional positional payload items (unused).
        **kwargs (Any): Additional keyword payload items (unused).
    """
    if not active_proxy():
        return

    try:
        logger.debug(f"TRAILS data received: {data}")
        # TODO: Implement aircraft trail/track visualization
    except Exception as e:
        logger.error(f"Error processing TRAILS data: {e}")

WebATM.proxy.handlers.navigation

WebATM.proxy.handlers.navigation

Navigation handler for DEFWPT (Define Waypoint) events.

on_defwpt_received

on_defwpt_received(data, *args, **kwargs)

Handle DEFWPT (define waypoint) events from the BlueSky server.

Currently only logs the payload; waypoint rendering in the web client is not yet implemented.

Parameters:

Name Type Description Default
data Any

The DEFWPT payload describing the waypoint.

required
*args Any

Additional positional payload items (unused).

()
**kwargs Any

Additional keyword payload items (unused).

{}
Source code in WebATM/proxy/handlers/navigation.py
def on_defwpt_received(data, *args, **kwargs):
    """Handle DEFWPT (define waypoint) events from the BlueSky server.

    Currently only logs the payload; waypoint rendering in the web client is
    not yet implemented.

    Args:
        data (Any): The DEFWPT payload describing the waypoint.
        *args (Any): Additional positional payload items (unused).
        **kwargs (Any): Additional keyword payload items (unused).
    """
    if not active_proxy():
        return

    try:
        logger.info(f"DEFWPT data received: {data}")
        # TODO: Implement waypoint definition handling
    except Exception as e:
        logger.error(f"Error processing DEFWPT data: {e}")