Skip to content

Proxy Managers

Each manager owns one concern of the proxy: connection lifecycle, node and server tracking, command processing, and data emission.

WebATM.proxy.managers.connection_manager

WebATM.proxy.managers.connection_manager

Connection management for the BlueSky proxy.

ConnectionManager

ConnectionManager(proxy)

Manage the BlueSky client connection lifecycle.

Owns creation and teardown of the network client, the 20 ms network update timer, data-flow timeout detection, and disconnection cleanup, following the ZMQ create-on-connect / destroy-on-close pattern.

Initialize the connection manager.

Parameters:

Name Type Description Default
proxy BlueSkyProxy

Parent proxy instance.

required
Source code in WebATM/proxy/managers/connection_manager.py
def __init__(self, proxy):
    """Initialize the connection manager.

    Args:
        proxy (BlueSkyProxy): Parent proxy instance.
    """
    self.proxy = proxy

start_client

start_client(hostname=None)

Start the network client with fresh state, following the ZMQ pattern.

Stops any existing connection first, creates the BlueSky network client if needed, wires its node/server signals to the node manager, connects to the server, and starts the network update and backup emission timers.

Parameters:

Name Type Description Default
hostname str | None

BlueSky server hostname/IP. When None, the proxy's currently configured server_ip is used.

None

Raises:

Type Description
RuntimeError

If the connection to the BlueSky server fails.

Exception

If the network client cannot be created.

Source code in WebATM/proxy/managers/connection_manager.py
def start_client(self, hostname=None):
    """Start the network client with fresh state, following the ZMQ pattern.

    Stops any existing connection first, creates the BlueSky network
    client if needed, wires its node/server signals to the node manager,
    connects to the server, and starts the network update and backup
    emission timers.

    Args:
        hostname (str | None): BlueSky server hostname/IP. When None, the
            proxy's currently configured ``server_ip`` is used.

    Raises:
        RuntimeError: If the connection to the BlueSky server fails.
        Exception: If the network client cannot be created.
    """
    # Ensure we start from a clean state. stop_client() destroys the client
    # instance, so this must run *before* we (re)create it below -- otherwise
    # we would connect() on a client that was just torn down.
    if self.proxy.running:
        logger.info("Stopping existing connection before starting new one")
        self.stop_client()
        time.sleep(0.2)

    # Following ZMQ pattern: create context and sockets when connecting
    if self.proxy.bluesky_client is None:
        logger.debug("Creating BlueSky network client...")
        try:
            self.proxy.bluesky_client = BlueSkyClient()
            self._connect_bluesky_client_signals()
            logger.info("BlueSky network client created successfully")
        except Exception as e:
            logger.error(f"Error creating BlueSky network client: {e}")
            raise

    if hostname:
        self.proxy.server_ip = hostname

    try:
        # Enable reconnection for this explicit connection attempt
        self.proxy.allow_reconnection = True

        logger.info(f"Connecting standalone proxy to '{self.proxy.server_ip}'...")
        try:
            success = self.proxy.bluesky_client.connect(
                hostname=self.proxy.server_ip
            )
            if not success:
                raise RuntimeError("Failed to connect to BlueSky server")
            logger.info(
                f"Network connection established with node ID: {safe_decode(self.proxy.bluesky_client.node_id)}"
            )
            logger.info("Waiting for BlueSky nodes to be detected...")
        except Exception as e:
            logger.error(f"Error in network connect(): {e}")
            raise

        # Initialize connection monitoring; was_connected flips to True
        # once the first node is detected.
        self.proxy.last_successful_update = time.time()
        self.proxy.was_connected = False

        self.proxy.running = True

        self._start_network_timer()
        self.proxy.data_mgr.start_backup_timer()

        logger.debug(
            f"Node detection started (timeout: {self.proxy.connection_timeout}s)"
        )
    except Exception as e:
        logger.error(
            f"Failed to connect to BlueSky server at '{self.proxy.server_ip}': {e}"
        )
        self.proxy.running = False
        self.proxy.allow_reconnection = False
        self.proxy.was_connected = False
        raise

mark_connected

mark_connected()

Flip the proxy to connected once the first node is detected.

Restarts the data-flow timeout clock from "first node appeared" — no data can arrive before nodes exist, so a slow cold start must not count against connection_timeout. Called from both the node-added signal and the network timer (whichever sees the first node first); a no-op when already connected.

Source code in WebATM/proxy/managers/connection_manager.py
def mark_connected(self):
    """Flip the proxy to connected once the first node is detected.

    Restarts the data-flow timeout clock from "first node appeared" — no
    data can arrive before nodes exist, so a slow cold start must not
    count against ``connection_timeout``. Called from both the node-added
    signal and the network timer (whichever sees the first node first);
    a no-op when already connected.
    """
    if self.proxy.was_connected:
        return
    self.proxy.was_connected = True
    self.proxy.last_successful_update = time.time()
    logger.info(
        f"Connection established to BlueSky server '{self.proxy.server_ip}'"
    )
    self.proxy._emit_connection_status(True)

close

close()

Close the network client's sockets and clear cached proxy state.

The client instance itself is kept (only stop_client destroys it); the app reconnects by creating a fresh BlueSkyProxy, so this only has to release the sockets and forget the cached server state.

Source code in WebATM/proxy/managers/connection_manager.py
def close(self):
    """Close the network client's sockets and clear cached proxy state.

    The client instance itself is kept (only ``stop_client`` destroys it);
    the app reconnects by creating a fresh ``BlueSkyProxy``, so this only
    has to release the sockets and forget the cached server state.
    """
    self.proxy.allow_reconnection = False

    client = self.proxy.bluesky_client
    if client is not None:
        try:
            client.close()
            logger.info("Network client closed successfully")
        except Exception as e:
            logger.error(f"Error closing network client: {e}")
        # Clear the active node so no stale data can be attributed to it.
        if hasattr(client, "act_id"):
            client.act_id = None

    self.proxy.data_mgr._reset_cached_state()
    logger.debug("Client state cleared and connections closed")

stop_client

stop_client(context='disconnect')

Stop the client with full cleanup and proper ZMQ error handling.

Cancels the network/backup timers, closes and destroys the network client, and clears remaining proxy state.

Parameters:

Name Type Description Default
context str

Cleanup context — "disconnect" for reconnection, "manual" for a user-initiated disconnect, "shutdown" for app termination.

'disconnect'
Source code in WebATM/proxy/managers/connection_manager.py
def stop_client(self, context="disconnect"):
    """Stop the client with full cleanup and proper ZMQ error handling.

    Cancels the network/backup timers, closes and destroys the network
    client, and clears remaining proxy state.

    Args:
        context (str): Cleanup context — ``"disconnect"`` for
            reconnection, ``"manual"`` for a user-initiated disconnect,
            ``"shutdown"`` for app termination.
    """
    if self.proxy.running:
        logger.info("Stopping BlueSky client connection")

    self.proxy.running = False
    self.proxy.allow_reconnection = False

    self._cancel_timers()
    self._close_bluesky_client()
    self.proxy.data_mgr._clear_state(context)

WebATM.proxy.managers.node_manager

WebATM.proxy.managers.node_manager

Node and server management for the BlueSky proxy.

NodeManager

NodeManager(proxy)

Track BlueSky simulation nodes and servers.

Reacts to node/server discovery and removal callbacks from the network client, keeps the proxy's tracked_nodes/tracked_servers maps in sync, detects server shutdown when all nodes disappear, and emits node_info updates to connected web clients.

Initialize the node manager.

Parameters:

Name Type Description Default
proxy BlueSkyProxy

Parent proxy instance.

required
Source code in WebATM/proxy/managers/node_manager.py
def __init__(self, proxy):
    """Initialize the node manager.

    Args:
        proxy (BlueSkyProxy): Parent proxy instance.
    """
    self.proxy = proxy

serialize_node_info

serialize_node_info()

Build the JSON-serializable node_info payload.

Decodes the binary node/server IDs kept in tracked_nodes and tracked_servers into the string forms the frontend expects (NodeData in frontend/src/data/types.ts). Used for both the node_info Socket.IO event and the initial_data snapshot.

Returns:

Type Description
dict

nodes, servers, active_node and total_nodes.

Source code in WebATM/proxy/managers/node_manager.py
def serialize_node_info(self):
    """Build the JSON-serializable ``node_info`` payload.

    Decodes the binary node/server IDs kept in ``tracked_nodes`` and
    ``tracked_servers`` into the string forms the frontend expects
    (``NodeData`` in ``frontend/src/data/types.ts``). Used for both the
    ``node_info`` Socket.IO event and the ``initial_data`` snapshot.

    Returns:
        dict: ``nodes``, ``servers``, ``active_node`` and ``total_nodes``.
    """
    # Iterate over snapshots: the network-timer thread mutates these maps
    # while Socket.IO threads (get_nodes, initial_data) serialize them.
    nodes_data = {}
    for node_id_str, tracked in list(self.proxy.tracked_nodes.items()):
        node_data = tracked.copy()
        if "node_id" in node_data:
            node_data["node_id"] = safe_decode(node_data["node_id"])
        if "server_id" in node_data:
            raw_server_id = node_data["server_id"]
            node_data["server_id"] = safe_decode(raw_server_id)
            node_data["server_id_hex"] = id2str(raw_server_id)
        nodes_data[node_id_str] = node_data

    servers_data = {}
    for server_id, tracked in list(self.proxy.tracked_servers.items()):
        server_data = tracked.copy()
        if "server_id" in server_data:
            server_data["server_id"] = safe_decode(server_data["server_id"])
        servers_data[safe_decode(server_id)] = server_data

    return {
        "nodes": nodes_data,
        "servers": servers_data,
        "active_node": self._get_safe_active_node(),
        "total_nodes": len(nodes_data),
    }

actnode

actnode(node_id)

Select the active simulation node via the network client.

Parameters:

Name Type Description Default
node_id bytes

ID of the node to make active.

required

Returns:

Type Description
Any

The result of BlueSkyClient.actnode.

Raises:

Type Description
RuntimeError

If the network client is not initialized.

Source code in WebATM/proxy/managers/node_manager.py
def actnode(self, node_id):
    """Select the active simulation node via the network client.

    Args:
        node_id (bytes): ID of the node to make active.

    Returns:
        Any: The result of ``BlueSkyClient.actnode``.

    Raises:
        RuntimeError: If the network client is not initialized.
    """
    if self.proxy.bluesky_client is None:
        raise RuntimeError("Network client not initialized")
    return self.proxy.bluesky_client.actnode(node_id)

addnodes

addnodes(count, server_id=None)

Request new simulation nodes from a BlueSky server.

Parameters:

Name Type Description Default
count int

Number of nodes to add.

required
server_id bytes | None

Server to add the nodes on. When None, the network client picks its default server.

None

Returns:

Type Description
Any

The result of BlueSkyClient.addnodes.

Raises:

Type Description
RuntimeError

If the network client is not initialized.

Source code in WebATM/proxy/managers/node_manager.py
def addnodes(self, count, server_id=None):
    """Request new simulation nodes from a BlueSky server.

    Args:
        count (int): Number of nodes to add.
        server_id (bytes | None): Server to add the nodes on. When None,
            the network client picks its default server.

    Returns:
        Any: The result of ``BlueSkyClient.addnodes``.

    Raises:
        RuntimeError: If the network client is not initialized.
    """
    if self.proxy.bluesky_client is None:
        raise RuntimeError("Network client not initialized")
    return self.proxy.bluesky_client.addnodes(count, server_id=server_id)

delnode

delnode(node_id)

Request termination of a single simulation node via DELNODE.

Parameters:

Name Type Description Default
node_id bytes

ID of the node to terminate.

required

Returns:

Type Description
Any

The result of BlueSkyClient.delnode.

Raises:

Type Description
RuntimeError

If the network client is not initialized.

Source code in WebATM/proxy/managers/node_manager.py
def delnode(self, node_id):
    """Request termination of a single simulation node via DELNODE.

    Args:
        node_id (bytes): ID of the node to terminate.

    Returns:
        Any: The result of ``BlueSkyClient.delnode``.

    Raises:
        RuntimeError: If the network client is not initialized.
    """
    if self.proxy.bluesky_client is None:
        raise RuntimeError("Network client not initialized")
    return self.proxy.bluesky_client.delnode(node_id)

WebATM.proxy.managers.command_processor

WebATM.proxy.managers.command_processor

Command processing and forwarding for the BlueSky proxy.

CommandProcessor

CommandProcessor(proxy)

Handle command processing, forwarding, and echo responses.

Queues user/GUI commands on the network client's stack, forwards them to the BlueSky server (answering bare HELP/? locally), and emits echo responses back to connected web clients.

Initialize the command processor.

Parameters:

Name Type Description Default
proxy BlueSkyProxy

Parent proxy instance.

required
Source code in WebATM/proxy/managers/command_processor.py
def __init__(self, proxy):
    """Initialize the command processor.

    Args:
        proxy (BlueSkyProxy): Parent proxy instance.
    """
    self.proxy = proxy

send_command

send_command(command: str) -> bool

Send a command to the simulation using stack processing.

Queues the command on the client stack and immediately processes the queue, forwarding to the BlueSky server as appropriate.

Parameters:

Name Type Description Default
command str

The stack command line to send (e.g. "CRE KL123 A320 52.3 4.7 90 FL100 250").

required

Returns:

Type Description
bool

True if the command was queued and processed, False if the BlueSky client is not running or an error occurred.

Source code in WebATM/proxy/managers/command_processor.py
def send_command(self, command: str) -> bool:
    """Send a command to the simulation using stack processing.

    Queues the command on the client stack and immediately processes the
    queue, forwarding to the BlueSky server as appropriate.

    Args:
        command (str): The stack command line to send (e.g. ``"CRE KL123
            A320 52.3 4.7 90 FL100 250"``).

    Returns:
        bool: True if the command was queued and processed, False if the
            BlueSky client is not running or an error occurred.
    """
    try:
        if self.proxy.bluesky_client and self.proxy.bluesky_client.running:
            self.proxy.bluesky_client.stack.stack(command)
            self._process_stack_commands()
            return True
        else:
            logger.warning("Cannot send command - BlueSky client not running")
            return False
    except Exception as e:
        logger.error(f"Command error for '{command}': {e}")
        return False

forward

forward(*cmdlines, target_id=None)

Forward one or more stack commands to the BlueSky server.

Mirrors BlueSky's stack.forward(): sends to the given target, the active node, or the server. Multiple commands may be passed as separate arguments and/or semicolon-separated within a single string.

Parameters:

Name Type Description Default
*cmdlines str

One or more stack command lines to forward.

()
target_id bytes | None

Explicit node/server ID to address. When None, falls back to the active node, then the server.

None
Source code in WebATM/proxy/managers/command_processor.py
def forward(self, *cmdlines, target_id=None):
    """Forward one or more stack commands to the BlueSky server.

    Mirrors BlueSky's ``stack.forward()``: sends to the given target, the
    active node, or the server. Multiple commands may be passed as
    separate arguments and/or semicolon-separated within a single string.

    Args:
        *cmdlines (str): One or more stack command lines to forward.
        target_id (bytes | None): Explicit node/server ID to address. When
            None, falls back to the active node, then the server.
    """
    if not cmdlines:
        return
    if not (self.proxy.bluesky_client and self.proxy.bluesky_client.running):
        logger.warning("Cannot forward - BlueSky client not running")
        return
    self._forward_command(";".join(cmdlines), target_id)

WebATM.proxy.managers.data_manager

WebATM.proxy.managers.data_manager

Data emission and state management for the BlueSky proxy.

DataManager

DataManager(proxy)

Manage Socket.IO data emission, backup timers, and state clearing.

Emits connection status, cleared-state payloads and periodic backup data to connected web clients, and provides the initial-page-load snapshot of the proxy's cached simulation state.

Initialize the data manager.

Parameters:

Name Type Description Default
proxy BlueSkyProxy

Parent proxy instance.

required
Source code in WebATM/proxy/managers/data_manager.py
def __init__(self, proxy):
    """Initialize the data manager.

    Args:
        proxy (BlueSkyProxy): Parent proxy instance.
    """
    self.proxy = proxy

start_backup_timer

start_backup_timer()

Start (or restart) the 0.5 s backup emission timer.

Source code in WebATM/proxy/managers/data_manager.py
def start_backup_timer(self):
    """Start (or restart) the 0.5 s backup emission timer."""
    if self.proxy.backup_timer:
        self.proxy.backup_timer.cancel()
    self.proxy.backup_timer = threading.Timer(0.5, self.backup_data_emit)
    self.proxy.backup_timer.daemon = True
    self.proxy.backup_timer.start()

backup_data_emit

backup_data_emit()

Re-emit cached sim/traffic data and reschedule the backup timer.

Safety net for web clients that connect between subscriber emissions: pushes the latest cached siminfo and acdata payloads, then schedules the next backup tick while the proxy is running.

Source code in WebATM/proxy/managers/data_manager.py
def backup_data_emit(self):
    """Re-emit cached sim/traffic data and reschedule the backup timer.

    Safety net for web clients that connect between subscriber emissions:
    pushes the latest cached ``siminfo`` and ``acdata`` payloads, then
    schedules the next backup tick while the proxy is running.
    """
    if not self.proxy.running:
        return

    if self.proxy.socketio and self.proxy.connected_clients > 0:
        try:
            if self.proxy.sim_data:
                self.proxy.socketio.emit("siminfo", self.proxy.sim_data)
            if self.proxy.traffic_data:
                self.proxy.socketio.emit("acdata", self.proxy.traffic_data)
        except Exception:
            pass

    self.start_backup_timer()

get_current_data

get_current_data() -> dict[str, Any]

Build the simulation state snapshot for an initial page load.

Shapes (polygons/polylines) are only included for the currently active node.

Returns:

Type Description
dict[str, Any]

Snapshot with traffic_data, sim_data, poly_data, polyline_data, cmddict, connection_status, node_info and a timestamp.

Source code in WebATM/proxy/managers/data_manager.py
def get_current_data(self) -> dict[str, Any]:
    """Build the simulation state snapshot for an initial page load.

    Shapes (polygons/polylines) are only included for the currently
    active node.

    Returns:
        dict[str, Any]: Snapshot with ``traffic_data``, ``sim_data``,
            ``poly_data``, ``polyline_data``, ``cmddict``,
            ``connection_status``, ``node_info`` and a ``timestamp``.
    """
    active_node_id = self.proxy.node_mgr._get_safe_active_node()

    poly_data = {}
    polyline_data = {}

    if active_node_id:
        # Only include shapes from the active node
        if active_node_id in self.proxy.poly_data_by_node:
            poly_data = self.proxy.poly_data_by_node[active_node_id]

        if active_node_id in self.proxy.polyline_data_by_node:
            polyline_data = self.proxy.polyline_data_by_node[active_node_id]

        poly_count = len(poly_data.get("polys", {}))
        polyline_count = len(polyline_data.get("polys", {}))
        if poly_count > 0 or polyline_count > 0:
            logger.info(
                f"Including shapes from active node '{active_node_id}' in initial data: {poly_count} polygons, {polyline_count} polylines"
            )
    else:
        logger.debug("No active node - not including any shapes in initial data")

    return {
        "traffic_data": self.proxy.traffic_data,
        "sim_data": self.proxy.sim_data,
        "poly_data": poly_data,
        "polyline_data": polyline_data,
        "cmddict": self.proxy.cmddict,
        "connection_status": {
            "connected": self.proxy.is_connected,
            "server_ip": self.proxy.server_ip,
            "last_update": self.proxy.last_successful_update,
        },
        # Same serialized shape as the node_info event (no raw bytes).
        "node_info": self.proxy.node_mgr.serialize_node_info(),
        "timestamp": time.time(),
    }