This repository is a Proof of Concept (PoC) project developed as part of the Google Summer of Code (GSoC). The goal of this project is to bring the core satellite management features of the Constellation framework's desktop-based MissionControl application to the web.
In this project, a custom controller class was written using the standard Constellation Python API. Live satellite management (real-time status tracking, sending commands, etc.) is successfully achieved through a web interface via WebSockets. There may be some issues within the scope of this PoC. These will be resolved during the development of the main project. While this PoC does not include all the capabilities of MissionControl, it aims to prove that a web-based architecture is viable.
The Constellation network uses the MissionControl application as the standard way to monitor and manage satellites. In the GIF below, you can see the standard usage of the original desktop application, satellite state management, and the current interface dynamics.
One of the strongest parts of the web-based solution we developed is its ability to provide real-time, two-way synchronization with the Constellation ecosystem.
In the GIF below, you can see the Web Application of this project and the original MissionControl window running at the same time. A state change triggered from the web interface is instantly seen in the MissionControl app. Similarly, any action taken in MissionControl updates immediately on the web interface without delay. This is strong proof that our architecture works perfectly with the existing network.
One important note here is that values like "Last Message", "Heartbeat", and "Lives" are not updated dynamically. To keep this PoC simple, these values are currently static. During the full project development, all this data will be handled dynamically and in sync. Only the outputs of functions starting with get_ are printed in the "Last Message" area to show what happens when a command like get_name is sent to a satellite. Not all commands from the MissionControl interface are active in the web interface yet. The goal is to build a pop-up identical to the one in MissionControl. In the background, other get_ functions were added to the list but have been commented out to hide them from the interface for now.
Also, you might notice that when a command like get_name is sent to a satellite, the output only appears on the client that sent the command. You can confirm this is not a synchronized action by opening two different MissionControl windows.
When you log into the web interface, it automatically lists the satellites in the network, and their statuses can be tracked live. When a new satellite is detected, it is added to the interface. If a satellite is removed, it automatically disappears from the dashboard.

The heart of this PoC project is the MyController class, which handles all satellite operations. Fetching satellite data, tracking state changes, and sending commands are all done through this class.
If you look at server/controller.py, you will see that MyController inherits from the ScriptableController class found in the Constellation Python API. The inheritance chain is as follows:
The basic structure of the MyController class looks like this:
class MyController(ScriptableController):
def __init__(self, on_update=None, **kwargs):
# ...
def _build_snapshot(self):
# ...
def _add_satellite(self, service):
# ...
def _remove_satellite(self, service):
# ...
def _poll_heartbeats(self):
# ...
def _dispatch(self, events):
# ...
def _emit(self):
# ...
def send_command(self, canonical_name, cmd, payload=None):
# ...
def get_satellite_details(self, canonical_name):
# ...
def get_all_satellites(self):
# ...Here is a quick summary of what these functions do:
-
_build_snapshot(): Takes a "snapshot" of the current state of the Constellation network. It holds the names, types, current states, and last modified times of the satellites. Example output:{ "Sputnik.Device1": { "name": "Device1", "type": "Sputnik", "state": "ORBIT", "last_changed": "2026-03-30 22:43:01.997005+00:00" } } -
_add_satellite()&_remove_satellite(): The Constellation framework automatically detects satellites in the network using the CHIRP protocol. Inherited fromBaseController, these methods trigger automatically when a new satellite joins or leaves the network. In this project, they are overridden to call the_emit()function, so the interface gets updated instantly. -
_poll_heartbeats(): Inherited from theHeartbeatCheckerclass, this method is triggered when a regular 'heartbeat' is received from the satellites. It calls the_emit()method every time to ensure the interface stays live. -
_emit(): This is the most critical method for keeping the interface updated in real-time using WebSockets. It compares the old snapshot with a new one created by_build_snapshot(). It finds:- Newly added satellites,
- Satellites that have disconnected (DEAD),
- Satellites that have changed their state.
Then, it packages them and sends them to the
_dispatch()method to be broadcast.
-
_dispatch(events): Notifies the outside world about the changes. It runs theon_updatecallback provided during class startup, sending the data to the WebSocket server (and then to the web interface). -
send_command(): The function needed to send a command with parameters to a specific satellite (e.g.,Sputnik.Device1).Note on Payload Data: In the current structure of this PoC, the
payloaddata is temporarily hardcoded. For example, when thestartcommand is triggered, a special fixed payload namedRUN_001is sent along with the command. This hardcoded approach is actually wrong. This structural issue will be fixed during the main project development. -
get_satellite_details()&get_all_satellites(): These functions retrieve data for a specific satellite or all satellites from the latest snapshot.
While MyController listens to satellite data in the background and sends it out using _dispatch(), the task of sending this data to the web interface instantly and without interruption belongs to the WebSocket server in server/ws.py. However, an important architectural problem must be solved here to keep the system stable.
Background operations coming from the Python API run on separate threads. In contrast, our WebSocket server, which needs to efficiently handle fast and multiple connections, runs asynchronously (asyncio). If these background threads try to directly and suddenly send data into the running asyncio WebSocket loop, they break thread safety, which will cause the application to crash.
To make these two different architectures work together safely, an asynchronous queue system is placed between them. Here is how ws.py works:
First, when MyController starts, it is told where to send updates:
# The push_events function is called when updates are triggered
ctrl = MyController(
group="test",
on_update=push_events
)When a new update comes from MyController, this data is not thrown directly to the users. Instead, it is pushed into a queue using the push_events() function:
def push_events(events):
if event_queue is not None and main_loop is not None:
# Data can't be written directly to the async queue from a different thread.
# This is why the data is safely placed in the queue using call_soon_threadsafe.
main_loop.call_soon_threadsafe(event_queue.put_nowait, events)The critical part here is the call_soon_threadsafe() method. This acts as a safe bridge that allows the background thread to drop its data into the asynchronous event_queue without locking or crashing the system.
Finally, the WebSocket server pulls this ready data from the queue one by one and broadcasts it to the users:
async def broadcast_events():
while True:
# Takes the next data when the WebSocket server is ready
events = await event_queue.get()
msg = json.dumps(events, default=str)
# Broadcasts the message to all connected web clients
if connected_clients:
websockets.broadcast(connected_clients, msg)In summary, instead of dangerously sending data directly over the network, background operations leave the data in this designated queue using a safe transfer method. The asynchronous WebSocket server then takes the data from the queue when it is its turn and smoothly broadcasts it to the clients.
The communication between the frontend client and the Python WebSocket server is structured using standard JSON formats. Below are the specific payload structures used for different operations.
When the user triggers an action from the web interface, the client sends a message with the send_command type. It contains the target satellite, the command to execute, and an optional payload.
For most commands (like initialize), no explicit payload is sent (handled as {} in the background):
{
"type": "send_command",
"cmd": "<command>",
"target": "<satellite_type>.<satellite_name>"
}However, as a specific case for this PoC, triggering the start command automatically includes a hardcoded payload:
{
"type": "send_command",
"cmd": "start",
"target": "<satellite_type>.<satellite_name>",
"payload": "RUN_001"
}The server sends three distinct types of messages back to the client:
Initial Snapshot (first_snapshot):
Sent immediately when a new client connects to the WebSocket. It contains the current state of all known satellites in the network as an array.
{
"type": "first_snapshot",
"data": [
{
"name": "<satellite_name>",
"type": "<satellite_type>",
"state": "<current_state>",
"last_changed": "<timestamp>"
}
]
}Command Responses (command_response):
When a client sends a specific command (such as initialize), the server processes it asynchronously and directly replies with the result.
{
"type": "command_response",
"cmd": "<command_name>",
"from": "<satellite_type>.<satellite_name>",
"data": {
"success": "<boolean>",
"message": "<response_message>",
"payload": "<payload_data_or_null>",
"meta": {},
"error": "<error_message>"
}
}Live State Updates:
When the MyController detects state changes in the Constellation network, it broadcasts an array of events to all connected clients.
Example: When a new satellite is added:
[
{
"event": "satellite_added",
"data": {
"name": "<satellite_name>",
"type": "<satellite_type>",
"state": "<initial_state>", // NEW
"last_changed": "<timestamp>"
}
}
]Example: When a satellite state changes:
[
{
"event": "satellite_changed",
"satellite": {
"name": "<satellite_name>",
"type": "<satellite_type>",
"state": "<new_state>",
"last_changed": "<timestamp>"
},
"previous_state": "<old_state>"
}
]Example: When a satellite is removed:
[
{
"event": "satellite_removed",
"satellite": {
"name": "<satellite_name>",
"type": "<satellite_type>",
"state": "<final_state>",
"last_changed": "<timestamp>"
}
}
]To run the project fully, both the frontend (Svelte) and backend (Python WebSocket server) must run at the same time.
Requirements:
- Node.js
- Python
- Git
git clone https://github.com/cysctl/constellation-web-poc.git
cd constellation-web-pocGo to the "server" folder:
cd serverIt is recommended to use a virtual environment:
python -m venv venv
source venv/bin/activate # LinuxInstall dependencies and start the server:
pip install -r requirements.txt
python ws.pyThe server will start listening on the local network.
Open a new terminal and stay in the root directory. Install dependencies and start the development server:
npm install
npm run devIf port 5173 is available, the app will run at:
Open this address in your browser and enter WebSocket settings from the top-right menu to start using the system. The application uses a Constellation group called test. You can change this by updating the following code block in the ws.py file:
ctrl = MyController(
group="test", # Constellation group
on_update=push_events
)AI assistants were utilized in the preparation of this README file and during the development of the front-end.
The design and architecture of the front-end were primarily adapted from my other project, DAQ Command Center.
This project is licensed under the MIT License. See the LICENSE file for more details.


