Skip to content

Proxy Core

The proxy gateway bridging the web interface with the BlueSky network client. BlueSkyProxy is a thin delegation layer composed of the managers; the handlers are registered against BlueSky topics by subscribers.

WebATM.proxy

WebATM.proxy

BlueSky proxy package for web interface communication.

This package provides the BlueSky proxy gateway that bridges the web interface with the BlueSky network client. It includes:

  • Core proxy class for client management
  • Event handlers for simulation data
  • Subscriber registration for network events

BlueSkyProxy

BlueSkyProxy()

Bridge between the web interface and the BlueSky network client.

Owns the network client lifecycle, caches incoming simulation data, and relays it to connected web clients over Socket.IO. The actual work is delegated to four focused managers following a composition pattern; only the methods that routes, Socket.IO handlers, network-event handlers or tests actually call through the proxy are re-exposed here — manager internals are reached via connection_mgr/node_mgr/ command_proc/data_mgr directly.

Attributes:

Name Type Description
bluesky_client BlueSkyClient | None

Active network client; created when connecting and destroyed on close.

running bool

Whether the network update loop is active.

socketio

Flask-SocketIO instance used to emit events to web clients.

traffic_data dict

Latest ACDATA payload, cached for new clients.

sim_data dict

Latest SIMINFO payload, cached for new clients.

tracked_nodes dict

Known simulation nodes keyed by hex node ID.

tracked_servers dict

Known servers keyed by raw server ID.

cmddict dict

Command dictionary mapping command names to their comma-separated argument signatures (seeded locally, replaced by the active node's STACKCMDS answer).

connection_mgr ConnectionManager

Connection lifecycle manager.

node_mgr NodeManager

Node/server tracking manager.

command_proc CommandProcessor

Command processing manager.

data_mgr DataManager

Data emission and state manager.

Initialize the proxy with empty caches and its manager modules.

Source code in WebATM/proxy/core.py
def __init__(self):
    """Initialize the proxy with empty caches and its manager modules."""
    logger.debug("Initializing BlueSkyProxy()...")

    # ZMQ pattern: the client is created on connect and destroyed on close.
    self.bluesky_client = None

    self.running = False
    self.network_timer = None
    self.socketio = None

    # Flag to prevent automatic reconnection
    self.allow_reconnection = False

    # Connection monitoring
    self.last_successful_update = time.time()
    self.connection_timeout = 10.0  # 10 seconds without updates = disconnected
    self.was_connected = False
    self.connection_failures = 0
    self.max_connection_failures = 3  # Max failures before marking disconnected

    # Data caches for web client
    self.traffic_data = {}
    self.sim_data = {}

    # Per-node shape stores (only the active node's shapes are displayed)
    self.poly_data_by_node = {}
    self.polyline_data_by_node = {}

    # Throttling for data emission (echo messages are never throttled)
    self.last_siminfo_emit = 0
    self.last_acdata_emit = 0
    self.last_node_info_emit = 0
    self.siminfo_interval = 0.1  # 10 Hz for sim info
    self.acdata_interval = 0.1  # 10 Hz for aircraft data
    self.node_info_interval = 1.0  # 1 Hz periodic refresh of the Nodes panel

    # Backup timer for data updates
    self.backup_timer = None

    # Track connected clients
    self.connected_clients = 0

    # Track nodes and servers like web client does
    self.tracked_nodes = {}
    self.tracked_servers = {}  # Keep minimal server tracking for compatibility

    # Store current map bounds
    self.current_bbox = None

    # Server IP address (overridden by main.py / the connect routes)
    self.server_ip = "localhost"

    # Command dictionary seeds, in the arg-signature format BlueSky's
    # STACKCMDS answers ship; replaced by the active node's STACKCMDS.
    self.cmddict = {
        "HELP": "[command]",
        "?": "[command]",
    }

    # Initialize managers
    self.connection_mgr = ConnectionManager(self)
    self.node_mgr = NodeManager(self)
    self.command_proc = CommandProcessor(self)
    self.data_mgr = DataManager(self)

is_connected property

is_connected: bool

Single source of truth for "are we connected to BlueSky".

True once the client is running, has previously detected at least one node, and still has active nodes. Every consumer (Socket.IO payloads, REST routes) should read this instead of re-deriving the formula.

Returns:

Type Description
bool

True when connected to a BlueSky server with active nodes.

start_client

start_client(hostname=None)

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

Parameters:

Name Type Description Default
hostname str

BlueSky server hostname or IP address. Defaults to the previously configured server address.

None

Raises:

Type Description
RuntimeError

If the connection to the BlueSky server fails.

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

    Args:
        hostname (str, optional): BlueSky server hostname or IP address.
            Defaults to the previously configured server address.

    Raises:
        RuntimeError: If the connection to the BlueSky server fails.
    """
    return self.connection_mgr.start_client(hostname)

stop_client

stop_client(context='disconnect')

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

Parameters:

Name Type Description Default
context str

Reason for stopping — "disconnect" for reconnection, "manual" for user disconnect, or "shutdown" for app termination.

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

    Args:
        context (str): Reason for stopping — "disconnect" for reconnection,
            "manual" for user disconnect, or "shutdown" for app termination.
    """
    return self.connection_mgr.stop_client(context)

close

close()

Close all network connections and clear state like BlueSky's close() method.

Source code in WebATM/proxy/core.py
def close(self):
    """Close all network connections and clear state like BlueSky's close() method."""
    return self.connection_mgr.close()

actnode

actnode(node_id)

Delegate actnode call to network proxy.

Source code in WebATM/proxy/core.py
def actnode(self, node_id):
    """Delegate actnode call to network proxy."""
    return self.node_mgr.actnode(node_id)

addnodes

addnodes(count, server_id=None)

Delegate addnodes call to network proxy.

Source code in WebATM/proxy/core.py
def addnodes(self, count, server_id=None):
    """Delegate addnodes call to network proxy."""
    return self.node_mgr.addnodes(count, server_id=server_id)

delnode

delnode(node_id)

Delegate delnode call to network proxy.

Source code in WebATM/proxy/core.py
def delnode(self, node_id):
    """Delegate delnode call to network proxy."""
    return self.node_mgr.delnode(node_id)

send_command

send_command(command: str) -> bool

Send a command to the simulation using stack processing.

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

forward

forward(*cmdlines, target_id=None)

Forward one or more stack commands to BlueSky server.

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

get_current_data

get_current_data() -> dict[str, Any]

Get current simulation data for initial page load.

Source code in WebATM/proxy/core.py
def get_current_data(self) -> dict[str, Any]:
    """Get current simulation data for initial page load."""
    return self.data_mgr.get_current_data()

register_subscribers

register_subscribers(proxy=None)

Register all handler callbacks with the proxy's BlueSky client.

Iterates over SUBSCRIPTIONS and subscribes each (topic, callback, actonly) triple on the proxy's network client. Topics flagged actonly only deliver data for the active node and are re-subscribed when the active node changes.

Logs an error and returns early if no proxy is available, or a warning if the proxy has no connected BlueSky client yet.

Parameters:

Name Type Description Default
proxy BlueSkyProxy | None

Proxy whose client to attach to. Defaults to the globally registered proxy. Callers that just connected a specific proxy instance should pass it explicitly, so a concurrent reconnect swapping the global can never leave the client they started without subscribers.

None
Source code in WebATM/proxy/subscribers.py
def register_subscribers(proxy=None):
    """Register all handler callbacks with the proxy's BlueSky client.

    Iterates over ``SUBSCRIPTIONS`` and subscribes each (topic, callback,
    actonly) triple on the proxy's network client. Topics flagged ``actonly``
    only deliver data for the active node and are re-subscribed when the
    active node changes.

    Logs an error and returns early if no proxy is available, or a warning
    if the proxy has no connected BlueSky client yet.

    Args:
        proxy (BlueSkyProxy | None): Proxy whose client to attach to. Defaults
            to the globally registered proxy. Callers that just connected a
            specific proxy instance should pass it explicitly, so a concurrent
            reconnect swapping the global can never leave the client they
            started without subscribers.
    """
    if proxy is None:
        # Imported lazily: WebATM.proxy imports this module while it is still
        # being initialised, so get_bluesky_proxy does not exist at module-load
        # time yet.
        from . import get_bluesky_proxy

        proxy = get_bluesky_proxy()
    if not proxy:
        logger.error("No proxy available for subscriber registration")
        return

    if not proxy.bluesky_client:
        logger.warning(
            "No BlueSky client available in BlueSky Proxy - Connect to BlueSky server via WebATM"
        )
        return

    logger.debug("Registering subscriber callbacks with standalone client...")
    try:
        for topic, callback, actonly in SUBSCRIPTIONS:
            proxy.bluesky_client.subscribe(topic, callback, actonly=actonly)
            logger.debug(f"Registered {topic} subscriber (actonly={actonly})")

        logger.info("All subscribers registered successfully with standalone client")
    except Exception as e:
        logger.error(f"Error registering subscribers: {e}")
        import traceback

        traceback.print_exc()

get_bluesky_proxy

get_bluesky_proxy()

Get the current BlueSky proxy instance.

Returns:

Type Description
BlueSkyProxy | None

The globally registered proxy instance, or None if no proxy has been set yet.

Source code in WebATM/proxy/__init__.py
def get_bluesky_proxy():
    """Get the current BlueSky proxy instance.

    Returns:
        BlueSkyProxy | None: The globally registered proxy instance, or None if
            no proxy has been set yet.
    """
    return _bluesky_proxy

set_bluesky_proxy

set_bluesky_proxy(proxy)

Set the global BlueSky proxy instance.

Parameters:

Name Type Description Default
proxy BlueSkyProxy | None

Proxy instance to register globally, or None to clear the current one.

required
Source code in WebATM/proxy/__init__.py
def set_bluesky_proxy(proxy):
    """Set the global BlueSky proxy instance.

    Args:
        proxy (BlueSkyProxy | None): Proxy instance to register globally, or
            None to clear the current one.
    """
    global _bluesky_proxy
    _bluesky_proxy = proxy

WebATM.proxy.core

WebATM.proxy.core

BlueSky proxy gateway for web interface communication.

BlueSkyProxy

BlueSkyProxy()

Bridge between the web interface and the BlueSky network client.

Owns the network client lifecycle, caches incoming simulation data, and relays it to connected web clients over Socket.IO. The actual work is delegated to four focused managers following a composition pattern; only the methods that routes, Socket.IO handlers, network-event handlers or tests actually call through the proxy are re-exposed here — manager internals are reached via connection_mgr/node_mgr/ command_proc/data_mgr directly.

Attributes:

Name Type Description
bluesky_client BlueSkyClient | None

Active network client; created when connecting and destroyed on close.

running bool

Whether the network update loop is active.

socketio

Flask-SocketIO instance used to emit events to web clients.

traffic_data dict

Latest ACDATA payload, cached for new clients.

sim_data dict

Latest SIMINFO payload, cached for new clients.

tracked_nodes dict

Known simulation nodes keyed by hex node ID.

tracked_servers dict

Known servers keyed by raw server ID.

cmddict dict

Command dictionary mapping command names to their comma-separated argument signatures (seeded locally, replaced by the active node's STACKCMDS answer).

connection_mgr ConnectionManager

Connection lifecycle manager.

node_mgr NodeManager

Node/server tracking manager.

command_proc CommandProcessor

Command processing manager.

data_mgr DataManager

Data emission and state manager.

Initialize the proxy with empty caches and its manager modules.

Source code in WebATM/proxy/core.py
def __init__(self):
    """Initialize the proxy with empty caches and its manager modules."""
    logger.debug("Initializing BlueSkyProxy()...")

    # ZMQ pattern: the client is created on connect and destroyed on close.
    self.bluesky_client = None

    self.running = False
    self.network_timer = None
    self.socketio = None

    # Flag to prevent automatic reconnection
    self.allow_reconnection = False

    # Connection monitoring
    self.last_successful_update = time.time()
    self.connection_timeout = 10.0  # 10 seconds without updates = disconnected
    self.was_connected = False
    self.connection_failures = 0
    self.max_connection_failures = 3  # Max failures before marking disconnected

    # Data caches for web client
    self.traffic_data = {}
    self.sim_data = {}

    # Per-node shape stores (only the active node's shapes are displayed)
    self.poly_data_by_node = {}
    self.polyline_data_by_node = {}

    # Throttling for data emission (echo messages are never throttled)
    self.last_siminfo_emit = 0
    self.last_acdata_emit = 0
    self.last_node_info_emit = 0
    self.siminfo_interval = 0.1  # 10 Hz for sim info
    self.acdata_interval = 0.1  # 10 Hz for aircraft data
    self.node_info_interval = 1.0  # 1 Hz periodic refresh of the Nodes panel

    # Backup timer for data updates
    self.backup_timer = None

    # Track connected clients
    self.connected_clients = 0

    # Track nodes and servers like web client does
    self.tracked_nodes = {}
    self.tracked_servers = {}  # Keep minimal server tracking for compatibility

    # Store current map bounds
    self.current_bbox = None

    # Server IP address (overridden by main.py / the connect routes)
    self.server_ip = "localhost"

    # Command dictionary seeds, in the arg-signature format BlueSky's
    # STACKCMDS answers ship; replaced by the active node's STACKCMDS.
    self.cmddict = {
        "HELP": "[command]",
        "?": "[command]",
    }

    # Initialize managers
    self.connection_mgr = ConnectionManager(self)
    self.node_mgr = NodeManager(self)
    self.command_proc = CommandProcessor(self)
    self.data_mgr = DataManager(self)

is_connected property

is_connected: bool

Single source of truth for "are we connected to BlueSky".

True once the client is running, has previously detected at least one node, and still has active nodes. Every consumer (Socket.IO payloads, REST routes) should read this instead of re-deriving the formula.

Returns:

Type Description
bool

True when connected to a BlueSky server with active nodes.

start_client

start_client(hostname=None)

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

Parameters:

Name Type Description Default
hostname str

BlueSky server hostname or IP address. Defaults to the previously configured server address.

None

Raises:

Type Description
RuntimeError

If the connection to the BlueSky server fails.

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

    Args:
        hostname (str, optional): BlueSky server hostname or IP address.
            Defaults to the previously configured server address.

    Raises:
        RuntimeError: If the connection to the BlueSky server fails.
    """
    return self.connection_mgr.start_client(hostname)

stop_client

stop_client(context='disconnect')

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

Parameters:

Name Type Description Default
context str

Reason for stopping — "disconnect" for reconnection, "manual" for user disconnect, or "shutdown" for app termination.

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

    Args:
        context (str): Reason for stopping — "disconnect" for reconnection,
            "manual" for user disconnect, or "shutdown" for app termination.
    """
    return self.connection_mgr.stop_client(context)

close

close()

Close all network connections and clear state like BlueSky's close() method.

Source code in WebATM/proxy/core.py
def close(self):
    """Close all network connections and clear state like BlueSky's close() method."""
    return self.connection_mgr.close()

actnode

actnode(node_id)

Delegate actnode call to network proxy.

Source code in WebATM/proxy/core.py
def actnode(self, node_id):
    """Delegate actnode call to network proxy."""
    return self.node_mgr.actnode(node_id)

addnodes

addnodes(count, server_id=None)

Delegate addnodes call to network proxy.

Source code in WebATM/proxy/core.py
def addnodes(self, count, server_id=None):
    """Delegate addnodes call to network proxy."""
    return self.node_mgr.addnodes(count, server_id=server_id)

delnode

delnode(node_id)

Delegate delnode call to network proxy.

Source code in WebATM/proxy/core.py
def delnode(self, node_id):
    """Delegate delnode call to network proxy."""
    return self.node_mgr.delnode(node_id)

send_command

send_command(command: str) -> bool

Send a command to the simulation using stack processing.

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

forward

forward(*cmdlines, target_id=None)

Forward one or more stack commands to BlueSky server.

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

get_current_data

get_current_data() -> dict[str, Any]

Get current simulation data for initial page load.

Source code in WebATM/proxy/core.py
def get_current_data(self) -> dict[str, Any]:
    """Get current simulation data for initial page load."""
    return self.data_mgr.get_current_data()

WebATM.proxy.subscribers

WebATM.proxy.subscribers

Subscriber registration for BlueSky network events.

Maps BlueSky data topics (SIMINFO, ACDATA, ECHO, ...) to the handler functions in :mod:WebATM.proxy.handlers and registers them with the active BlueSky network client.

register_subscribers

register_subscribers(proxy=None)

Register all handler callbacks with the proxy's BlueSky client.

Iterates over SUBSCRIPTIONS and subscribes each (topic, callback, actonly) triple on the proxy's network client. Topics flagged actonly only deliver data for the active node and are re-subscribed when the active node changes.

Logs an error and returns early if no proxy is available, or a warning if the proxy has no connected BlueSky client yet.

Parameters:

Name Type Description Default
proxy BlueSkyProxy | None

Proxy whose client to attach to. Defaults to the globally registered proxy. Callers that just connected a specific proxy instance should pass it explicitly, so a concurrent reconnect swapping the global can never leave the client they started without subscribers.

None
Source code in WebATM/proxy/subscribers.py
def register_subscribers(proxy=None):
    """Register all handler callbacks with the proxy's BlueSky client.

    Iterates over ``SUBSCRIPTIONS`` and subscribes each (topic, callback,
    actonly) triple on the proxy's network client. Topics flagged ``actonly``
    only deliver data for the active node and are re-subscribed when the
    active node changes.

    Logs an error and returns early if no proxy is available, or a warning
    if the proxy has no connected BlueSky client yet.

    Args:
        proxy (BlueSkyProxy | None): Proxy whose client to attach to. Defaults
            to the globally registered proxy. Callers that just connected a
            specific proxy instance should pass it explicitly, so a concurrent
            reconnect swapping the global can never leave the client they
            started without subscribers.
    """
    if proxy is None:
        # Imported lazily: WebATM.proxy imports this module while it is still
        # being initialised, so get_bluesky_proxy does not exist at module-load
        # time yet.
        from . import get_bluesky_proxy

        proxy = get_bluesky_proxy()
    if not proxy:
        logger.error("No proxy available for subscriber registration")
        return

    if not proxy.bluesky_client:
        logger.warning(
            "No BlueSky client available in BlueSky Proxy - Connect to BlueSky server via WebATM"
        )
        return

    logger.debug("Registering subscriber callbacks with standalone client...")
    try:
        for topic, callback, actonly in SUBSCRIPTIONS:
            proxy.bluesky_client.subscribe(topic, callback, actonly=actonly)
            logger.debug(f"Registered {topic} subscriber (actonly={actonly})")

        logger.info("All subscribers registered successfully with standalone client")
    except Exception as e:
        logger.error(f"Error registering subscribers: {e}")
        import traceback

        traceback.print_exc()