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()
        # Wait for cleanup to complete
        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
    logger.info(
        "Attempting to connect to BlueSky remote server hosted by amvlab..."
    )

    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
        self.proxy.last_successful_update = time.time()
        self.proxy.was_connected = (
            False  # Will be set to True when nodes are detected
        )

        self.proxy.running = True

        # Start network timer (like web client does with timer)
        self._start_network_timer()

        # Start backup data emission 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 remote server hosted by amvlab: {e}"
        )
        self.proxy.running = False
        self.proxy.allow_reconnection = False
        self.proxy.was_connected = False
        raise

close

close()

Close all network connections and clear cached proxy state.

Mirrors BlueSky's own close(): shuts the network client's sockets (the ZMQ context is left to the client), resets connection monitoring, and clears tracked nodes/servers, data caches, emission timestamps, and the pending command dictionary.

Source code in WebATM/proxy/managers/connection_manager.py
def close(self):
    """Close all network connections and clear cached proxy state.

    Mirrors BlueSky's own ``close()``: shuts the network client's sockets
    (the ZMQ context is left to the client), resets connection monitoring,
    and clears tracked nodes/servers, data caches, emission timestamps,
    and the pending command dictionary.
    """
    # Disable reconnection first
    self.proxy.allow_reconnection = False

    # Just close the network client - don't destroy ZMQ context
    # The app creates a completely new BlueSkyProxy instance for reconnection
    try:
        logger.debug(" Closing network client...")
        if self.proxy.bluesky_client:
            self.proxy.bluesky_client.close()
        logger.info(" Network client closed successfully")
    except Exception as e:
        logger.error(f" Error closing network client: {e}")

    # We reuse the same network client instance - just close its sockets

    # Reset connection monitoring
    self.proxy.was_connected = False
    self.proxy.last_successful_update = time.time()

    # Clear all tracked state
    self.proxy.tracked_nodes.clear()
    self.proxy.tracked_servers.clear()

    # Clear active node reference to prevent showing corrupted data
    if hasattr(self.proxy.bluesky_client, "act_id"):
        self.proxy.bluesky_client.act_id = None

    # Clear data caches
    self.proxy.traffic_data = {}
    self.proxy.sim_data = {}
    self.proxy.echo_data = {}

    # Reset emission timestamps
    self.proxy.last_siminfo_emit = 0
    self.proxy.last_acdata_emit = 0
    self.proxy.last_node_info_emit = 0

    # Clear current map bounds
    self.proxy.current_bbox = None

    # Clear command dictionary
    self.proxy.cmddict.clear()

    # Following ZMQ pattern: clear client reference after closing
    # (new client will be created when reconnecting)

    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  # Disable reconnection when stopping

    # Cancel timers with proper cleanup
    self._cancel_timers()

    # Close network client with proper ZMQ error handling
    self._close_bluesky_client()

    # Clear remaining state
    self.proxy.data_mgr._clear_state(context)

reconnect

reconnect(hostname=None)

Reconnect to the BlueSky server with fresh ZMQ resources.

Stops the current client, clears state, then starts a new connection via start_client.

Parameters:

Name Type Description Default
hostname str | None

BlueSky server hostname/IP to reconnect to. When None, the previously configured host is reused.

None

Raises:

Type Description
Exception

Propagated from start_client if reconnection fails.

Source code in WebATM/proxy/managers/connection_manager.py
def reconnect(self, hostname=None):
    """Reconnect to the BlueSky server with fresh ZMQ resources.

    Stops the current client, clears state, then starts a new connection
    via ``start_client``.

    Args:
        hostname (str | None): BlueSky server hostname/IP to reconnect
            to. When None, the previously configured host is reused.

    Raises:
        Exception: Propagated from ``start_client`` if reconnection fails.
    """
    logger.info("Reconnecting to BlueSky server...")

    # Following ZMQ pattern: close sockets and destroy context first
    self.stop_client("disconnect")

    # Wait briefly for ZMQ cleanup to complete
    time.sleep(0.2)

    # Clear state and prepare for fresh connection
    self.proxy.data_mgr._clear_state()

    # Following ZMQ pattern: create fresh context and sockets
    try:
        self.start_client(hostname=hostname)
        logger.info(" Reconnection successful with fresh ZMQ resources")
    except Exception as e:
        logger.error(f" Reconnection failed: {e}")
        raise

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

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

    try:
        command_str = ";".join(cmdlines)
        target = self._resolve_target(target_id)

        if self.proxy.bluesky_client and self.proxy.bluesky_client.running:
            self.proxy.bluesky_client.send("STACK", command_str, target)
            logger.info(f"Forwarded to {target}: {command_str}")
        else:
            logger.warning("Cannot forward - BlueSky client not running")

    except Exception as e:
        logger.error(f"Error in forward(): {e}")
        self._echo_response(f"Error forwarding command: {e}", 1)

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
    )  # More frequent backup
    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:
            # Force emit current data
            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:
            # Handle emission errors gracefully (e.g., disconnected clients)
            pass

    # Schedule next backup emission
    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, echo_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``,
            ``echo_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")

    from ...bluesky_client import safe_decode

    return {
        "traffic_data": self.proxy.traffic_data,
        "sim_data": self.proxy.sim_data,
        "echo_data": self.proxy.echo_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,
        },
        "node_info": {
            "nodes": self.proxy.tracked_nodes.copy(),
            "servers": {
                safe_decode(k): v for k, v in self.proxy.tracked_servers.items()
            },
            "active_node": active_node_id,
            "total_nodes": len(self.proxy.tracked_nodes),
        },
        "timestamp": time.time(),
    }