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.

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.

echo_data dict

Latest echo message, 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 BlueSky's STACKCMDS broadcast).

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()...")

    # Don't initialize BlueSky client in __init__ - create when needed
    # Following ZMQ pattern: create context and sockets only when connecting
    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 = {}
    self.echo_data = {}

    # Store POLY data by node ID
    self.poly_data_by_node = {}

    # Store POLYLINE data by node ID
    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

    # Store server IP address (default to localhost, will be set by main.py if configured)
    self.server_ip = "localhost"

    # Stack command processing (BlueSky client pattern). Values use the
    # comma-separated arg-signature format that BlueSky's STACKCMDS
    # broadcast ships (e.g. "acid,type,lat,lon,hdg,alt,spd"); these
    # seeds get overwritten the moment STACKCMDS arrives.
    self.cmddict = {
        "HELP": "[command]",
        "?": "[command]",
    }  # Local command dictionary (like Command.cmddict)

    # 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)

reconnect

reconnect(hostname=None)

Reconnect to BlueSky server following ZMQ pattern.

Source code in WebATM/proxy/core.py
def reconnect(self, hostname=None):
    """Reconnect to BlueSky server following ZMQ pattern."""
    return self.connection_mgr.reconnect(hostname)

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)

start_backup_timer

start_backup_timer()

Start backup timer to ensure data gets sent regularly.

Source code in WebATM/proxy/core.py
def start_backup_timer(self):
    """Start backup timer to ensure data gets sent regularly."""
    return self.data_mgr.start_backup_timer()

backup_data_emit

backup_data_emit()

Backup method to emit data if subscribers haven't.

Source code in WebATM/proxy/core.py
def backup_data_emit(self):
    """Backup method to emit data if subscribers haven't."""
    return self.data_mgr.backup_data_emit()

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()

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

Iterates over SUBSCRIPTIONS and subscribes each (topic, callback, actonly) triple on the global 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 global proxy is set, or a warning if the proxy has no connected BlueSky client yet.

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

    Iterates over ``SUBSCRIPTIONS`` and subscribes each (topic, callback,
    actonly) triple on the global 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 global proxy is set, or a warning
    if the proxy has no connected BlueSky client yet.
    """
    # 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.

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.

echo_data dict

Latest echo message, 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 BlueSky's STACKCMDS broadcast).

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()...")

    # Don't initialize BlueSky client in __init__ - create when needed
    # Following ZMQ pattern: create context and sockets only when connecting
    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 = {}
    self.echo_data = {}

    # Store POLY data by node ID
    self.poly_data_by_node = {}

    # Store POLYLINE data by node ID
    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

    # Store server IP address (default to localhost, will be set by main.py if configured)
    self.server_ip = "localhost"

    # Stack command processing (BlueSky client pattern). Values use the
    # comma-separated arg-signature format that BlueSky's STACKCMDS
    # broadcast ships (e.g. "acid,type,lat,lon,hdg,alt,spd"); these
    # seeds get overwritten the moment STACKCMDS arrives.
    self.cmddict = {
        "HELP": "[command]",
        "?": "[command]",
    }  # Local command dictionary (like Command.cmddict)

    # 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)

reconnect

reconnect(hostname=None)

Reconnect to BlueSky server following ZMQ pattern.

Source code in WebATM/proxy/core.py
def reconnect(self, hostname=None):
    """Reconnect to BlueSky server following ZMQ pattern."""
    return self.connection_mgr.reconnect(hostname)

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)

start_backup_timer

start_backup_timer()

Start backup timer to ensure data gets sent regularly.

Source code in WebATM/proxy/core.py
def start_backup_timer(self):
    """Start backup timer to ensure data gets sent regularly."""
    return self.data_mgr.start_backup_timer()

backup_data_emit

backup_data_emit()

Backup method to emit data if subscribers haven't.

Source code in WebATM/proxy/core.py
def backup_data_emit(self):
    """Backup method to emit data if subscribers haven't."""
    return self.data_mgr.backup_data_emit()

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()

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

Iterates over SUBSCRIPTIONS and subscribes each (topic, callback, actonly) triple on the global 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 global proxy is set, or a warning if the proxy has no connected BlueSky client yet.

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

    Iterates over ``SUBSCRIPTIONS`` and subscribes each (topic, callback,
    actonly) triple on the global 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 global proxy is set, or a warning
    if the proxy has no connected BlueSky client yet.
    """
    # 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()