multimodalsim-viewer 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multimodalsim_viewer-0.0.1/PKG-INFO +21 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/__init__.py +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/__init__.py +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/http_routes.py +125 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/log_manager.py +15 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/scripts.py +72 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/server.py +210 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/server_utils.py +129 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation.py +154 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation_manager.py +607 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation_visualization_data_collector.py +756 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/server/simulation_visualization_data_model.py +1693 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/__init__.py +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/cli.py +45 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/server.py +44 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/bitmap-fonts/custom-sans-serif.xml +1 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-MTC2LSCT.js +1 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/chunk-U5CGW4P4.js +7 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/favicon.ico +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/control-bar.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/sample-bus.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/sample-stop.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/sample-wait.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/simulation-control-bar.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/zoom-out-passenger.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/images/zoom-out-vehicle.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/index.html +15 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/main-X7OVCS3N.js +3648 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/media/layers-2x-TBM42ERR.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/media/layers-55W3Q4RM.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/media/marker-icon-2V3QKKVC.png +0 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/polyfills-FFHMD2TL.js +2 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer/ui/static/styles-KU7LTPET.css +1 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/PKG-INFO +21 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/SOURCES.txt +40 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/dependency_links.txt +1 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/entry_points.txt +8 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/requires.txt +9 -0
- multimodalsim_viewer-0.0.1/multimodalsim_viewer.egg-info/top_level.txt +1 -0
- multimodalsim_viewer-0.0.1/setup.cfg +4 -0
- multimodalsim_viewer-0.0.1/setup.py +44 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: multimodalsim_viewer
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Multimodal simulation viewer
|
5
|
+
License: MIT
|
6
|
+
Keywords: flask angular ui multimodal server
|
7
|
+
Requires-Python: ==3.11.*
|
8
|
+
Requires-Dist: flask==3.1.1
|
9
|
+
Requires-Dist: flask-socketio==5.5.1
|
10
|
+
Requires-Dist: eventlet==0.40.0
|
11
|
+
Requires-Dist: websocket-client==1.8.0
|
12
|
+
Requires-Dist: filelock==3.18.0
|
13
|
+
Requires-Dist: flask_cors==6.0.0
|
14
|
+
Requires-Dist: questionary==2.1.0
|
15
|
+
Requires-Dist: python-dotenv==1.1.0
|
16
|
+
Requires-Dist: multimodalsim==0.0.1
|
17
|
+
Dynamic: keywords
|
18
|
+
Dynamic: license
|
19
|
+
Dynamic: requires-dist
|
20
|
+
Dynamic: requires-python
|
21
|
+
Dynamic: summary
|
File without changes
|
File without changes
|
@@ -0,0 +1,125 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import zipfile
|
4
|
+
from flask import Blueprint, request, jsonify, send_file
|
5
|
+
import logging
|
6
|
+
import tempfile
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
10
|
+
data_dir = BASE_DIR / "data"
|
11
|
+
saved_simulations_dir = Path(__file__).parent / "saved_simulations"
|
12
|
+
|
13
|
+
http_routes = Blueprint("http_routes", __name__)
|
14
|
+
|
15
|
+
# MARK: Zip Management
|
16
|
+
|
17
|
+
def get_unique_folder_name(base_path, folder_name):
|
18
|
+
counter = 1
|
19
|
+
original_name = folder_name
|
20
|
+
while os.path.exists(os.path.join(base_path, folder_name)):
|
21
|
+
folder_name = f"{original_name}_({counter})"
|
22
|
+
counter += 1
|
23
|
+
return folder_name
|
24
|
+
|
25
|
+
def zip_folder(folder_path, zip_name):
|
26
|
+
if not os.path.isdir(folder_path):
|
27
|
+
return None
|
28
|
+
|
29
|
+
zip_path = os.path.join(tempfile.gettempdir(), f"{zip_name}.zip")
|
30
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
31
|
+
for root, _, files in os.walk(folder_path):
|
32
|
+
for file in files:
|
33
|
+
file_path = os.path.join(root, file)
|
34
|
+
zipf.write(file_path, os.path.relpath(file_path, folder_path))
|
35
|
+
|
36
|
+
return zip_path
|
37
|
+
|
38
|
+
def handle_zip_upload(folder_path):
|
39
|
+
parent_dir = os.path.dirname(folder_path)
|
40
|
+
base_folder_name = os.path.basename(folder_path)
|
41
|
+
|
42
|
+
unique_folder_name = get_unique_folder_name(parent_dir, base_folder_name)
|
43
|
+
actual_folder_path = os.path.join(parent_dir, unique_folder_name)
|
44
|
+
|
45
|
+
os.makedirs(actual_folder_path, exist_ok=True)
|
46
|
+
|
47
|
+
if 'file' not in request.files:
|
48
|
+
return jsonify({"error": "No file part"}), 400
|
49
|
+
|
50
|
+
file = request.files['file']
|
51
|
+
if file.filename == '':
|
52
|
+
return jsonify({"error": "No selected file"}), 400
|
53
|
+
|
54
|
+
zip_path = os.path.join(tempfile.gettempdir(), file.filename)
|
55
|
+
file.save(zip_path)
|
56
|
+
|
57
|
+
try:
|
58
|
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
59
|
+
zip_ref.extractall(actual_folder_path)
|
60
|
+
logging.info(f"Extracted files: {zip_ref.namelist()}")
|
61
|
+
|
62
|
+
os.remove(zip_path)
|
63
|
+
except zipfile.BadZipFile:
|
64
|
+
return jsonify({"error": "Invalid ZIP file"}), 400
|
65
|
+
|
66
|
+
response_message = f"Folder '{unique_folder_name}' uploaded successfully"
|
67
|
+
if unique_folder_name != base_folder_name:
|
68
|
+
response_message += f" (renamed from '{base_folder_name}')"
|
69
|
+
|
70
|
+
return jsonify({
|
71
|
+
"message": response_message,
|
72
|
+
"actual_folder_name": unique_folder_name
|
73
|
+
}), 201
|
74
|
+
|
75
|
+
# MARK: Input Data Routes
|
76
|
+
@http_routes.route("/api/input_data/<folder_name>", methods=["GET"])
|
77
|
+
def export_input_data(folder_name):
|
78
|
+
folder_path = os.path.join(data_dir, folder_name)
|
79
|
+
logging.info(f"Requested folder: {folder_path}")
|
80
|
+
|
81
|
+
zip_path = zip_folder(folder_path, folder_name)
|
82
|
+
if not zip_path:
|
83
|
+
return jsonify({"error": "Folder not found"}), 404
|
84
|
+
|
85
|
+
return send_file(zip_path, as_attachment=True)
|
86
|
+
|
87
|
+
@http_routes.route("/api/input_data/<folder_name>", methods=["POST"])
|
88
|
+
def import_input_data(folder_name):
|
89
|
+
folder_path = os.path.join(data_dir, folder_name)
|
90
|
+
return handle_zip_upload(folder_path)
|
91
|
+
|
92
|
+
@http_routes.route("/api/input_data/<folder_name>", methods=["DELETE"])
|
93
|
+
def delete_input_data(folder_name):
|
94
|
+
folder_path = os.path.join(data_dir, folder_name)
|
95
|
+
if not os.path.isdir(folder_path):
|
96
|
+
return jsonify({"error": "Folder not found"}), 404
|
97
|
+
|
98
|
+
shutil.rmtree(folder_path)
|
99
|
+
return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
|
100
|
+
|
101
|
+
# MARK: Saved Simulations Routes
|
102
|
+
@http_routes.route("/api/simulation/<folder_name>", methods=["GET"])
|
103
|
+
def export_saved_simulation(folder_name):
|
104
|
+
folder_path = os.path.join(saved_simulations_dir, folder_name)
|
105
|
+
logging.info(f"Requested folder: {folder_path}")
|
106
|
+
|
107
|
+
zip_path = zip_folder(folder_path, folder_name)
|
108
|
+
if not zip_path:
|
109
|
+
return jsonify({"error": "Folder not found"}), 404
|
110
|
+
|
111
|
+
return send_file(zip_path, as_attachment=True)
|
112
|
+
|
113
|
+
@http_routes.route("/api/simulation/<folder_name>", methods=["POST"])
|
114
|
+
def import_saved_simulation(folder_name):
|
115
|
+
folder_path = os.path.join(saved_simulations_dir, folder_name)
|
116
|
+
return handle_zip_upload(folder_path)
|
117
|
+
|
118
|
+
@http_routes.route("/api/simulation/<folder_name>", methods=["DELETE"])
|
119
|
+
def delete_saved_simulation(folder_name):
|
120
|
+
folder_path = os.path.join(saved_simulations_dir, folder_name)
|
121
|
+
if not os.path.isdir(folder_path):
|
122
|
+
return jsonify({"error": "Folder not found"}), 404
|
123
|
+
|
124
|
+
shutil.rmtree(folder_path)
|
125
|
+
return jsonify({"message": f"Folder '{folder_name}' deleted successfully"})
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
|
4
|
+
def register_log(simulation_id, message):
|
5
|
+
current_directory = os.path.dirname(os.path.abspath(__file__))
|
6
|
+
log_directory_name = "saved_logs"
|
7
|
+
log_directory_path = f"{current_directory}/{log_directory_name}"
|
8
|
+
file_name = f"{simulation_id}.txt"
|
9
|
+
file_path = f"{log_directory_path}/{file_name}"
|
10
|
+
|
11
|
+
if not os.path.exists(log_directory_path):
|
12
|
+
os.makedirs(log_directory_path)
|
13
|
+
|
14
|
+
with open(file_path, "a") as file:
|
15
|
+
file.write(message + "\n")
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import threading
|
2
|
+
import time
|
3
|
+
|
4
|
+
import requests
|
5
|
+
from multimodalsim_viewer.server.server import run_server
|
6
|
+
from multimodalsim_viewer.server.server_utils import CLIENT_PORT, HOST, PORT
|
7
|
+
from multimodalsim_viewer.ui.cli import main as run_ui
|
8
|
+
from socketio import Client, exceptions
|
9
|
+
|
10
|
+
|
11
|
+
def run_server_and_ui():
|
12
|
+
# Start the server in a separate thread
|
13
|
+
server_thread = threading.Thread(target=run_server)
|
14
|
+
server_thread.start()
|
15
|
+
|
16
|
+
# Start the UI in a separate thread
|
17
|
+
ui_thread = threading.Thread(target=run_ui)
|
18
|
+
ui_thread.start()
|
19
|
+
|
20
|
+
# Wait for both threads to finish
|
21
|
+
server_thread.join()
|
22
|
+
ui_thread.join()
|
23
|
+
|
24
|
+
|
25
|
+
def terminate_server():
|
26
|
+
print("Terminating server...")
|
27
|
+
|
28
|
+
sio = Client()
|
29
|
+
|
30
|
+
try:
|
31
|
+
sio.connect(f"http://{HOST}:{PORT}", auth={"type": "script"})
|
32
|
+
|
33
|
+
sio.emit("terminate")
|
34
|
+
|
35
|
+
time.sleep(1)
|
36
|
+
|
37
|
+
sio.disconnect()
|
38
|
+
|
39
|
+
print("Server terminated")
|
40
|
+
|
41
|
+
except exceptions.ConnectionError as e:
|
42
|
+
print(f"Failed to connect to server (server not running?): {e}")
|
43
|
+
|
44
|
+
except Exception as e:
|
45
|
+
print(f"Error: {e}")
|
46
|
+
|
47
|
+
|
48
|
+
def terminate_ui():
|
49
|
+
print("Terminating UI...")
|
50
|
+
|
51
|
+
try:
|
52
|
+
response = requests.get(f"http://{HOST}:{CLIENT_PORT}/terminate")
|
53
|
+
|
54
|
+
if response.status_code == 200:
|
55
|
+
print("UI terminated")
|
56
|
+
else:
|
57
|
+
print(f"Failed to terminate UI: {response.status_code}")
|
58
|
+
|
59
|
+
except requests.exceptions.RequestException as e:
|
60
|
+
print(f"Error: {e}")
|
61
|
+
|
62
|
+
except Exception as e:
|
63
|
+
print(f"Error: {e}")
|
64
|
+
|
65
|
+
|
66
|
+
def terminate_all():
|
67
|
+
print("Terminating all...")
|
68
|
+
|
69
|
+
terminate_server()
|
70
|
+
terminate_ui()
|
71
|
+
|
72
|
+
print("All terminated")
|
@@ -0,0 +1,210 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
|
4
|
+
from flask import Flask
|
5
|
+
from flask_cors import CORS
|
6
|
+
from flask_socketio import SocketIO, emit, join_room, leave_room
|
7
|
+
from multimodalsim_viewer.server.http_routes import http_routes
|
8
|
+
from multimodalsim_viewer.server.server_utils import (
|
9
|
+
CLIENT_ROOM,
|
10
|
+
HOST,
|
11
|
+
PORT,
|
12
|
+
get_available_data,
|
13
|
+
get_session_id,
|
14
|
+
log,
|
15
|
+
)
|
16
|
+
from multimodalsim_viewer.server.simulation_manager import SimulationManager
|
17
|
+
|
18
|
+
|
19
|
+
def run_server():
|
20
|
+
app = Flask(__name__)
|
21
|
+
|
22
|
+
# Register HTTP routes
|
23
|
+
CORS(app)
|
24
|
+
app.register_blueprint(http_routes)
|
25
|
+
|
26
|
+
socketio = SocketIO(app, cors_allowed_origins="*")
|
27
|
+
|
28
|
+
# key = session id, value = auth type
|
29
|
+
sockets_types_by_session_id = dict()
|
30
|
+
|
31
|
+
simulation_manager = SimulationManager()
|
32
|
+
|
33
|
+
# MARK: Main events
|
34
|
+
@socketio.on("connect")
|
35
|
+
def on_connect(auth):
|
36
|
+
auth_type = auth["type"]
|
37
|
+
log("connected", auth_type)
|
38
|
+
sockets_types_by_session_id[get_session_id()] = auth_type
|
39
|
+
join_room(auth_type)
|
40
|
+
|
41
|
+
@socketio.on("disconnect")
|
42
|
+
def on_disconnect(reason):
|
43
|
+
session_id = get_session_id()
|
44
|
+
auth_type = sockets_types_by_session_id.pop(session_id)
|
45
|
+
log(f"disconnected: {reason}", auth_type)
|
46
|
+
leave_room(auth_type)
|
47
|
+
|
48
|
+
if auth_type == "simulation":
|
49
|
+
simulation_manager.on_simulation_disconnect(session_id)
|
50
|
+
|
51
|
+
# MARK: Client events
|
52
|
+
@socketio.on("start-simulation")
|
53
|
+
def on_client_start_simulation(name, data, response_event, max_duration):
|
54
|
+
log(
|
55
|
+
f"starting simulation {name} with data {data}, response event {response_event} and max duration {max_duration}",
|
56
|
+
"client",
|
57
|
+
)
|
58
|
+
simulation_manager.start_simulation(name, data, response_event, max_duration)
|
59
|
+
|
60
|
+
@socketio.on("stop-simulation")
|
61
|
+
def on_client_stop_simulation(simulation_id):
|
62
|
+
log(f"stopping simulation {simulation_id}", "client")
|
63
|
+
simulation_manager.stop_simulation(simulation_id)
|
64
|
+
|
65
|
+
@socketio.on("pause-simulation")
|
66
|
+
def on_client_pause_simulation(simulation_id):
|
67
|
+
log(f"pausing simulation {simulation_id}", "client")
|
68
|
+
simulation_manager.pause_simulation(simulation_id)
|
69
|
+
|
70
|
+
@socketio.on("resume-simulation")
|
71
|
+
def on_client_resume_simulation(simulation_id):
|
72
|
+
log(f"resuming simulation {simulation_id}", "client")
|
73
|
+
simulation_manager.resume_simulation(simulation_id)
|
74
|
+
|
75
|
+
@socketio.on("get-simulations")
|
76
|
+
def on_client_get_simulations():
|
77
|
+
log("getting simulations", "client")
|
78
|
+
simulation_manager.emit_simulations()
|
79
|
+
|
80
|
+
@socketio.on("get-available-data")
|
81
|
+
def on_client_get_data():
|
82
|
+
log("getting available data", "client")
|
83
|
+
emit("available-data", get_available_data(), to=CLIENT_ROOM)
|
84
|
+
|
85
|
+
@socketio.on("get-missing-simulation-states")
|
86
|
+
def on_client_get_missing_simulation_states(
|
87
|
+
simulation_id, visualization_time, loaded_state_orders
|
88
|
+
):
|
89
|
+
log(
|
90
|
+
f"getting missing simulation states for {simulation_id} with visualization time {visualization_time} and {len(loaded_state_orders)} loaded state orders ",
|
91
|
+
"client",
|
92
|
+
)
|
93
|
+
simulation_manager.emit_missing_simulation_states(
|
94
|
+
simulation_id, visualization_time, loaded_state_orders
|
95
|
+
)
|
96
|
+
|
97
|
+
@socketio.on("get-polylines")
|
98
|
+
def on_client_get_polylines(simulation_id):
|
99
|
+
log(f"getting polylines for {simulation_id}", "client")
|
100
|
+
simulation_manager.emit_simulation_polylines(simulation_id)
|
101
|
+
|
102
|
+
@socketio.on("edit-simulation-configuration")
|
103
|
+
def on_client_edit_simulation_configuration(simulation_id, max_duration):
|
104
|
+
log(
|
105
|
+
f"editing simulation {simulation_id} configuration with max duration {max_duration}",
|
106
|
+
"client",
|
107
|
+
)
|
108
|
+
simulation_manager.edit_simulation_configuration(simulation_id, max_duration)
|
109
|
+
|
110
|
+
# MARK: Script events
|
111
|
+
@socketio.on("terminate")
|
112
|
+
def on_script_terminate():
|
113
|
+
log("terminating server", "script")
|
114
|
+
|
115
|
+
for simulation_id, simulation_handler in simulation_manager.simulations.items():
|
116
|
+
if simulation_handler.process is not None:
|
117
|
+
simulation_manager.stop_simulation(simulation_id)
|
118
|
+
simulation_handler.process.join()
|
119
|
+
|
120
|
+
# TODO Solution to remove sleep
|
121
|
+
# - Add a flag to the simulation manager to stop the server
|
122
|
+
# - On simulation-end, check if all simulations with processes are stopped
|
123
|
+
# - If so, stop the server
|
124
|
+
time.sleep(1)
|
125
|
+
|
126
|
+
socketio.stop()
|
127
|
+
|
128
|
+
# MARK: Simulation events
|
129
|
+
@socketio.on("simulation-start")
|
130
|
+
def on_simulation_start(simulation_id, simulation_start_time):
|
131
|
+
log(f"simulation {simulation_id} started", "simulation")
|
132
|
+
simulation_manager.on_simulation_start(
|
133
|
+
simulation_id, get_session_id(), simulation_start_time
|
134
|
+
)
|
135
|
+
|
136
|
+
@socketio.on("simulation-pause")
|
137
|
+
def on_simulation_pause(simulation_id):
|
138
|
+
log(f"simulation {simulation_id} paused", "simulation")
|
139
|
+
simulation_manager.on_simulation_pause(simulation_id)
|
140
|
+
|
141
|
+
@socketio.on("simulation-resume")
|
142
|
+
def on_simulation_resume(simulation_id):
|
143
|
+
log(f"simulation {simulation_id} resumed", "simulation")
|
144
|
+
simulation_manager.on_simulation_resume(simulation_id)
|
145
|
+
|
146
|
+
@socketio.on("log")
|
147
|
+
def on_simulation_log(simulation_id, message):
|
148
|
+
log(f"simulation {simulation_id}: {message}", "simulation", logging.DEBUG)
|
149
|
+
|
150
|
+
@socketio.on("simulation-update-time")
|
151
|
+
def on_simulation_update_time(simulation_id, timestamp):
|
152
|
+
log(
|
153
|
+
f"simulation {simulation_id} time: {timestamp}",
|
154
|
+
"simulation",
|
155
|
+
logging.DEBUG,
|
156
|
+
)
|
157
|
+
simulation_manager.on_simulation_update_time(simulation_id, timestamp)
|
158
|
+
|
159
|
+
@socketio.on("simulation-update-estimated-end-time")
|
160
|
+
def on_simulation_update_estimated_end_time(simulation_id, estimated_end_time):
|
161
|
+
log(
|
162
|
+
f"simulation {simulation_id} estimated end time: {estimated_end_time}",
|
163
|
+
"simulation",
|
164
|
+
logging.DEBUG,
|
165
|
+
)
|
166
|
+
simulation_manager.on_simulation_update_estimated_end_time(
|
167
|
+
simulation_id, estimated_end_time
|
168
|
+
)
|
169
|
+
|
170
|
+
@socketio.on("simulation-update-polylines-version")
|
171
|
+
def on_simulation_update_polylines_version(simulation_id):
|
172
|
+
log(f"simulation {simulation_id} polylines version updated", "simulation")
|
173
|
+
|
174
|
+
simulation_manager.on_simulation_update_polylines_version(simulation_id)
|
175
|
+
|
176
|
+
@socketio.on("simulation-identification")
|
177
|
+
def on_simulation_identification(
|
178
|
+
simulation_id,
|
179
|
+
data,
|
180
|
+
simulation_start_time,
|
181
|
+
timestamp,
|
182
|
+
estimated_end_time,
|
183
|
+
max_duration,
|
184
|
+
status,
|
185
|
+
):
|
186
|
+
log(
|
187
|
+
f"simulation {simulation_id} identified with data {data}, simulation start time {simulation_start_time}, timestamp {timestamp}, estimated end time {estimated_end_time}, max duration {max_duration} and status {status}",
|
188
|
+
"simulation",
|
189
|
+
)
|
190
|
+
simulation_manager.on_simulation_identification(
|
191
|
+
simulation_id,
|
192
|
+
data,
|
193
|
+
simulation_start_time,
|
194
|
+
timestamp,
|
195
|
+
estimated_end_time,
|
196
|
+
max_duration,
|
197
|
+
status,
|
198
|
+
get_session_id(),
|
199
|
+
)
|
200
|
+
|
201
|
+
logging.basicConfig(level=logging.DEBUG)
|
202
|
+
|
203
|
+
log(f"Starting server at {HOST}:{PORT}", "server", should_emit=False)
|
204
|
+
|
205
|
+
# MARK: Run server
|
206
|
+
socketio.run(app, host=HOST, port=PORT)
|
207
|
+
|
208
|
+
|
209
|
+
if __name__ == "__main__":
|
210
|
+
run_server()
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import threading
|
5
|
+
from enum import Enum
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
from flask import request
|
10
|
+
from flask_socketio import emit
|
11
|
+
|
12
|
+
# Load environment variables from the root .env file
|
13
|
+
env_path = Path(__file__).parent.parent / ".env"
|
14
|
+
load_dotenv(env_path)
|
15
|
+
|
16
|
+
HOST = os.getenv("SERVER_HOST", "127.0.0.1")
|
17
|
+
PORT = int(os.getenv("PORT_SERVER", "8089")) # It will use .env or default to 8089
|
18
|
+
CLIENT_PORT = int(
|
19
|
+
os.getenv("PORT_CLIENT", "8085")
|
20
|
+
) # It will use .env or default to 8085
|
21
|
+
|
22
|
+
CLIENT_ROOM = "client"
|
23
|
+
SIMULATION_ROOM = "simulation"
|
24
|
+
SCRIPT_ROOM = "script"
|
25
|
+
|
26
|
+
# Save the state of the simulation every STATE_SAVE_STEP events
|
27
|
+
STATE_SAVE_STEP = 1000
|
28
|
+
|
29
|
+
# If the version is identical, the save file can be loaded
|
30
|
+
SAVE_VERSION = 9
|
31
|
+
|
32
|
+
SIMULATION_SAVE_FILE_SEPARATOR = "---"
|
33
|
+
|
34
|
+
|
35
|
+
class SimulationStatus(Enum):
|
36
|
+
STARTING = "starting"
|
37
|
+
PAUSED = "paused"
|
38
|
+
RUNNING = "running"
|
39
|
+
STOPPING = "stopping"
|
40
|
+
COMPLETED = "completed"
|
41
|
+
LOST = "lost"
|
42
|
+
CORRUPTED = "corrupted"
|
43
|
+
OUTDATED = "outdated"
|
44
|
+
FUTURE = "future"
|
45
|
+
|
46
|
+
|
47
|
+
RUNNING_SIMULATION_STATUSES = [
|
48
|
+
SimulationStatus.STARTING,
|
49
|
+
SimulationStatus.RUNNING,
|
50
|
+
SimulationStatus.PAUSED,
|
51
|
+
SimulationStatus.STOPPING,
|
52
|
+
SimulationStatus.LOST,
|
53
|
+
]
|
54
|
+
|
55
|
+
|
56
|
+
def get_session_id():
|
57
|
+
return request.sid
|
58
|
+
|
59
|
+
|
60
|
+
def build_simulation_id(name: str) -> tuple[str, str]:
|
61
|
+
# Get the current time
|
62
|
+
start_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S%f")
|
63
|
+
# Remove microseconds
|
64
|
+
start_time = start_time[:-3]
|
65
|
+
|
66
|
+
# Start time first to sort easily
|
67
|
+
simulation_id = f"{start_time}{SIMULATION_SAVE_FILE_SEPARATOR}{name}"
|
68
|
+
return simulation_id, start_time
|
69
|
+
|
70
|
+
|
71
|
+
def get_data_directory_path(data: str | None = None) -> str:
|
72
|
+
cwd = os.getcwd()
|
73
|
+
data_directory = os.path.join(cwd, "data")
|
74
|
+
|
75
|
+
if data is not None:
|
76
|
+
data_directory = os.path.join(data_directory, data)
|
77
|
+
|
78
|
+
return data_directory
|
79
|
+
|
80
|
+
|
81
|
+
def get_available_data():
|
82
|
+
data_dir = get_data_directory_path()
|
83
|
+
|
84
|
+
if not os.path.exists(data_dir):
|
85
|
+
return []
|
86
|
+
|
87
|
+
return os.listdir(data_dir)
|
88
|
+
|
89
|
+
|
90
|
+
def log(message: str, auth_type: str, level=logging.INFO, should_emit=True) -> None:
|
91
|
+
if auth_type == "server":
|
92
|
+
logging.log(level, f"[{auth_type}] {message}")
|
93
|
+
if should_emit:
|
94
|
+
emit("log", f"{level} [{auth_type}] {message}", to=CLIENT_ROOM)
|
95
|
+
else:
|
96
|
+
logging.log(level, f"[{auth_type}] {get_session_id()} {message}")
|
97
|
+
if should_emit:
|
98
|
+
emit(
|
99
|
+
"log",
|
100
|
+
f"{level} [{auth_type}] {get_session_id()} {message}",
|
101
|
+
to=CLIENT_ROOM,
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
def verify_simulation_name(name: str | None) -> str | None:
|
106
|
+
if name is None:
|
107
|
+
return "Name is required"
|
108
|
+
elif len(name) < 3:
|
109
|
+
return "Name must be at least 3 characters"
|
110
|
+
elif len(name) > 50:
|
111
|
+
return "Name must be at most 50 characters"
|
112
|
+
elif name.count(SIMULATION_SAVE_FILE_SEPARATOR) > 0:
|
113
|
+
return "Name must not contain three consecutive dashes"
|
114
|
+
elif any(char in name for char in ["/", "\\", ":", "*", "?", '"', "<", ">", "|"]):
|
115
|
+
return 'The name muse not contain characters that might affect the file system (e.g. /, \, :, *, ?, ", <, >, |)'
|
116
|
+
return None
|
117
|
+
|
118
|
+
|
119
|
+
def set_event_on_input(action: str, key: str, event: threading.Event) -> None:
|
120
|
+
try:
|
121
|
+
user_input = ""
|
122
|
+
while user_input != key:
|
123
|
+
user_input = input(f"Press {key} to {action}: ")
|
124
|
+
|
125
|
+
except EOFError:
|
126
|
+
pass
|
127
|
+
|
128
|
+
print(f"Received {key}: {action}")
|
129
|
+
event.set()
|