diff --git a/.gitignore b/.gitignore
index 06c2ec35..71384602 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ server/src/.vscode/
**/.vscode
**/.cache
**.project
-**jsconfig.json
\ No newline at end of file
+**jsconfig.json
+**/.env
diff --git a/README.md b/README.md
index 1da7c5ec..36704a33 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ libgcrypt, libpthread, libglib-2.0, libyaml,
* KIELER command line compiler: [kico.jar](https://rtsys.informatik.uni-kiel.de/~kieler/files/nightly/sccharts/cli/)
* Path to the folder that contains `kico.jar` has to be defined in the environment variable `KIELER_PATH`
* BahnDSL command line compiler: [bahnc](https://github.com/trinnguyen/bahndsl)
- * Path to the folder that contains the `bahnc` has to be defined in the environment variable `BAHNDSL_PATH`
+ * Path to the folder that contains the `bahnc` has to be defined in the environment variable `BAHNC_PATH`
* SCCharts verifier: [SWTbahn Verifier (SWT internal repository)](https://gitlab.rz.uni-bamberg.de/swt/swtbahn-verifier)
#### Command Line Client
@@ -70,9 +70,9 @@ client from a different working directory for the current session, otherwise it
won't find the configuration file. If you really want to use two logically
different clients on the same device, you could just run the clients from
different working directories which will lead to distinct configs.
-3. Now you can use the commands from the categories `swtbahn admin`,
-`swtbahn controller`, `swtbahn driver` and `swtbahn monitor`. If the system was
-not started by `swtbahn admin startup`, all the other commands won't work.
+3. Now you can use the commands from the roles `admin`,
+`controller`, `driver` and `monitor`. If the system was
+not started by `admin startup`, all the other commands won't work.
For example:
`./swtbahn admin startup`
@@ -230,12 +230,16 @@ As an example, when a request is made to set point10 to the normal state, the re
> _Intervening log messages from internal processing_
> LOG_NOTICE: `Request: Set point - point: point10 state: normal - finish`
-If the above request was instead made with an unsupported state, e.g., `foobar`, then the request handler would generate the following log messages to say that the processing was stopped because of invalid parameters:
+If the above request was instead made with an unsupported state, e.g., `foobar`, then the request handler would generate the following log messages to say that the processing was stopped because of invalid parameter values:
> LOG_NOTICE: `Request: Set point - point: point10 state: foobar - start`
> _Intervening log messages from internal processing_
-> LOG_ERR: `Request: Set point - point: point10 state: foobar - invalid parameters - abort`
+> LOG_ERR: `Request: Set point - point: point10 state: foobar - invalid parameter values - abort`
-If the above request forgot to specify the state, i.e., the state parameter is `null`, then the request handler would only generate the following log message to say that the parameter validation failed:
+If the above request forgot to specify the state, i.e., the state parameter is `null`, then the request handler would only generate the following log message to say that the parameter is missing:
-> LOG_ERR: `Request: Set point - invalid parameters`
\ No newline at end of file
+> LOG_ERR: `Request: Set point - missing parameter state`
+
+And if sending this info to the client failed, the following would be logged:
+
+> LOG_ERR: `Request: Set point - missing parameter state - but sending msg to client failed`
\ No newline at end of file
diff --git a/client/swtbahn b/client/swtbahn
index e75fb83c..696d3c60 100755
--- a/client/swtbahn
+++ b/client/swtbahn
@@ -2,7 +2,7 @@
"""
-Copyright (C) 2017 University of Bamberg, Software Technologies Research Group
+Copyright (C) 2025 University of Bamberg, Software Technologies Research Group
,
This file is part of the SWTbahn command line interface (swtbahn-cli), which is
@@ -24,10 +24,11 @@ The following people contributed to the conception and realization of the
present swtbahn-cli (in alphabetic order by surname):
- Nicolas Gross
+- Bernhard Luedtke
"""
-import click, yaml, requests
+import click, yaml, json, requests
@@ -43,13 +44,11 @@ session_id = 0
grab_id = 0
server = ""
-
-
# -----------------------
# --- config handling ---
# -----------------------
-def create_config(hostname, port, default_output):
+def create_config(hostname, port, default_output, do_test_connection=True):
try:
data = {'hostname': hostname, 'port': port,
'default_track_output': default_output, 'session_id': 0,
@@ -60,20 +59,22 @@ def create_config(hostname, port, default_output):
except Exception as e:
click.echo(e, err=True)
return
- global server
- server = "http://" + hostname + ":" + str(port)
- click.echo("Connection test:")
- try:
- response = requests.get(server)
- if (response.status_code != requests.codes.ok):
- click.echo("No SWTbahn server on " + server, err=True)
- else:
- click.echo("Running SWTbahn server found on " + server)
- except requests.exceptions.RequestException as e:
- click.echo(e, err=True)
+ if do_test_connection:
+ global server
+ server = "http://" + hostname + ":" + str(port)
+ click.echo("Connection test:")
+ try:
+ response = requests.get(server)
+ if (response.status_code != 200):
+ click.echo("No SWTbahn server on " + server, err=True)
+ else:
+ click.echo("SWTbahn server found on " + server)
+ except requests.exceptions.RequestException as e:
+ click.echo(e, err=True)
def update_session_and_grab_id(session_id, grab_id):
+ global config_file
try:
with open(config_file) as infile:
config = yaml.safe_load(infile)
@@ -85,22 +86,36 @@ def update_session_and_grab_id(session_id, grab_id):
click.echo(e, err=True)
+# Returns True if config is valid, otherwise returns False
def parse_config():
+ global config_file, hostname, port, default_track_output, session_id, grab_id, server
try:
with open(config_file) as infile:
config = yaml.safe_load(infile)
- global hostname, port, default_track_output, session_id, grab_id, server
hostname = config['hostname']
port = config['port']
default_track_output = config['default_track_output']
session_id = config['session_id']
grab_id = config['grab_id']
server = "http://" + hostname + ":" + str(port)
- return False
- except Exception as e:
return True
+ except Exception as e:
+ return False
+# -----------------------
+# --- logging helpers ---
+# -----------------------
+def log_common_feedback_err_msg(response_text:str, internal_err=False):
+ response_json: dict = json.loads(response_text)
+ click.echo(("Internal error" if internal_err else "Error") + ", server msg: "
+ + response_json.get("msg", ""), err=True)
+
+def log_not_running_or_unknown_err(status_code:int):
+ if (status_code == 503):
+ click.echo("System is not running.", err=True)
+ else:
+ click.echo(f"Unknown error, reply status code: {status_code}", err=True)
# ------------------------------
# --- command line interface ---
@@ -108,19 +123,38 @@ def parse_config():
@click.group(help="Command-line interface for controlling the SWTbahn. "
- "Use the config command first to set the hostname and port")
+ "Use the config command first to set the hostname and port."
+ "Use the reset_grab_and_session_id command if the server says they are invalid, "
+ "this may happen if an admin force-releases a train you've grabbed.")
def cli():
pass
-@click.command(help="Configure hostname and port")
-@click.argument('hostname')
-@click.argument('port', type=click.IntRange(0, 65535))
-@click.argument('default_track_output')
+@click.command(help="Configure hostname, port, and track output")
+@click.argument('hostname', required=True)
+@click.argument('port', type=click.IntRange(0, 65535), required=True)
+@click.argument('default_track_output', default="master")
def config(hostname, port, default_track_output):
create_config(hostname, port, default_track_output)
+@click.command(help="Reset your train ownership (grab and session id). "
+ "Do NOT do this if you still have proper control over the train!")
+def reset_grab_and_session_id():
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
+ else:
+ global server, session_id, grab_id
+ try:
+ # to be safe, try to release the train with the current grab id before resetting it
+ requests.post(server + "/driver/release-train",
+ data = {'session-id': session_id, 'grab-id': grab_id})
+ # Reset the session_id and grab_id
+ update_session_and_grab_id(0, -1)
+ except Exception as e:
+ click.echo(f"reset_grab_and_session_id failed. Error: {e}", err=True)
+
+
# --- admin ---
@click.group(help="Admin functionality")
@@ -130,42 +164,46 @@ def admin():
@click.command(help="Start up the SWTbahn")
def startup():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/admin/startup")
- if (response.status_code == requests.codes.ok):
- click.echo("System has started up\n")
+ if (response.status_code == 200):
+ click.echo("System has started up.")
+ elif (response.status_code == 409):
+ click.echo("System already running!")
+ elif (response.status_code == 500):
+ log_common_feedback_err_msg(response.text, internal_err=True)
else:
- click.echo("System already running!\n", err=True)
+ click.echo(f"Unknown error, reply status code: {response.status_code}", err=True)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Shut down the SWTbahn")
def shutdown():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/admin/shutdown")
- if (response.status_code == requests.codes.ok):
- click.echo("System is stopping\n")
+ if (response.status_code == 200):
+ click.echo("System has been stopped.")
update_session_and_grab_id(0, -1)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Set the power state of a track output")
+@click.command(help="Set the power state of all track outputs")
@click.argument('state', type=click.Choice(['off', 'stop', 'soft_stop', 'go']))
def set_track_output(state):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
@@ -177,75 +215,83 @@ def set_track_output(state):
}
response = requests.post(server + "/admin/set-track-output",
data = {'state': mapping.get(state)})
- if (response.status_code == requests.codes.ok):
- click.echo("Track output state set\n")
+ if (response.status_code == 200):
+ click.echo(f"Track output state set to {state}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(help="Set whether plugins will be verified upon upload")
@click.argument('verification-option', type=click.Choice(['true', 'false']))
def set_verification_option(verification_option):
- if(parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/admin/set-verification-option",
data = {'verification-option': verification_option})
- if (response.status_code == requests.codes.ok):
- click.echo("Verification option set to " + verification_option + "\n")
+ if (response.status_code == 200):
+ click.echo(f"Verification option set to {verification_option}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(help="Set the url of the verification server to use for verification on upload")
@click.argument('verification-url', type=click.STRING)
def set_verification_url(verification_url):
- if(parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/admin/set-verification-url",
data = {'verification-url': verification_url})
- if (response.status_code == requests.codes.ok):
- click.echo("Verification url set to " + verification_url + "\n")
+ if (response.status_code == 200):
+ click.echo(f"Verification url set to {verification_url}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running!\n", err=True)
+ click.echo(f"Unknown error, reply status code: {response.status_code}", err=True)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(name="release", help="Release a train")
@click.argument('train')
def admin_release(train):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/admin/release-train",
- data = {'train': train})
- if (response.status_code == requests.codes.ok):
- click.echo("Released grabbed train\n")
+ response = requests.post(server + "/admin/release-train", data = {'train': train})
+ if (response.status_code == 200):
+ click.echo(f"Released grabbed train {train}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or train not grabbed!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(name="set-dcc-speed", help="Set the dcc speed step of a train")
-@click.option('--backwards', '-b', help="The rear end of the train moves forward",
- is_flag=True)
-@click.option('--track_output', '-t', help="Use another track output than the "
- "default one", default="")
+@click.option('--backwards', '-b', help="The rear end of the train moves forward", is_flag=True)
+@click.option('--track_output', '-t', help="Use another track output than the default one", default="")
@click.argument('train')
@click.argument('speed', type=click.IntRange(0, 126))
def admin_set_dcc_speed(train, backwards, speed, track_output):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server, default_track_output
if (backwards and speed > 0):
@@ -254,12 +300,14 @@ def admin_set_dcc_speed(train, backwards, speed, track_output):
track_output = default_track_output
try:
response = requests.post(server + "/admin/set-dcc-train-speed",
- data = {'train': train, 'speed': speed, 'track-output': track_output})
- if (response.status_code == requests.codes.ok):
- click.echo("DCC train speed set to " + str(speed) + "\n")
+ data = {'train': train, 'speed': speed,
+ 'track-output': default_track_output})
+ if (response.status_code == 200):
+ click.echo(f"DCC speed of {train} set to {speed}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -274,37 +322,39 @@ def controller():
@click.command(help="Release a route")
@click.argument('route-id', type=int)
def release_route(route_id):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
try:
response = requests.post(server + "/controller/release-route",
- data = {'route-id': route_id})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Route " + str(route_id) + " released\n")
+ data = {'route-id': route_id})
+ if (response.status_code == 200):
+ click.echo(f"Route {route_id} released.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Switch a point")
+@click.command(help="Set a point")
@click.argument('point')
@click.argument('state')
def set_point(point, state):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/controller/set-point",
- data = {'point': point, 'state': state})
- if (response.status_code == requests.codes.ok):
- click.echo("Point " + point + " set to " + state + " \n")
+ data = {'point': point, 'state': state})
+ if (response.status_code == 200):
+ click.echo(f"Point {point} set to {state}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -313,18 +363,19 @@ def set_point(point, state):
@click.argument('signal')
@click.argument('state')
def set_signal(signal, state):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/controller/set-signal",
- data = {'signal': signal, 'state': state})
- if (response.status_code == requests.codes.ok):
- click.echo("Signal " + signal + " set to " + state + " \n")
+ data = {'signal': signal, 'state': state})
+ if (response.status_code == 200):
+ click.echo(f"Signal {signal} set to {state}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -333,35 +384,38 @@ def set_signal(signal, state):
@click.argument('peripheral')
@click.argument('state')
def set_peripheral(peripheral, state):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/controller/set-peripheral",
- data = {'peripheral': peripheral, 'state': state})
- if (response.status_code == requests.codes.ok):
- click.echo("Peripheral " + peripheral + " set to " + state + " \n")
+ data = {'peripheral': peripheral, 'state': state})
+ if (response.status_code == 200):
+ click.echo(f"Peripheral {peripheral} set to {state}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get the interlocker in use")
def get_interlocker():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/controller/get-interlocker")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text + "\n")
+ response = requests.get(server + "/controller/get-interlocker")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo("Interlocker in use: " + response_json.get("interlocker", None))
+ elif (response.status_code == 404):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -369,18 +423,20 @@ def get_interlocker():
@click.command(help="Set an interlocker to use")
@click.argument('interlocker')
def set_interlocker(interlocker):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/controller/set-interlocker",
- data = {'interlocker': interlocker})
- if (response.status_code == requests.codes.ok):
- click.echo("Interlocker set to " + interlocker + " \n")
+ data = {'interlocker': interlocker})
+ if (response.status_code == 200):
+ click.echo(f"Interlocker set to {interlocker}.")
+ elif (response.status_code == 400 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -388,37 +444,43 @@ def set_interlocker(interlocker):
@click.command(help="Unset an interlocker from use")
@click.argument('interlocker')
def unset_interlocker(interlocker):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/controller/unset-interlocker",
- data = {'interlocker': interlocker})
- if (response.status_code == requests.codes.ok):
- click.echo("Interlocker " + interlocker + " is unset \n")
+ data = {'interlocker': interlocker})
+ if (response.status_code == 200):
+ click.echo(f"Interlocker {interlocker} has been unset.")
+ elif (response.status_code == 400 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Upload an interlocker. Absolute path to an BahnDSL (*.bahn) file")
+@click.command(help="Upload an interlocker")
@click.argument('filepath', type=click.Path(exists=True))
def upload_interlocker(filepath):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/upload/interlocker",
- files = {'file': open(filepath, 'r')})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Interlocker uploaded\n")
+ files = {'file': open(filepath, 'r')})
+ if (response.status_code == 200):
+ click.echo("Interlocker has been uploaded.")
+ elif (response.status_code == 400 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ log_common_feedback_err_msg(response.text, internal_err=True)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -426,35 +488,21 @@ def upload_interlocker(filepath):
@click.command(help="Delete an interlocker")
@click.argument('interlocker-name')
def delete_interlocker(interlocker_name):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/upload/remove-interlocker",
- data = {'interlocker-name': interlocker_name})
- if (response.status_code == requests.codes.ok):
- click.echo("Interlocker " + interlocker_name + " deleted\n")
- else:
- click.echo("Interlocker still in use, or system not running or invalid state!\n",
- err=True)
- except requests.exceptions.RequestException as e:
- click.echo(e, err=True)
-
-
-@click.command(help="Get a list of interlockers")
-def get_interlocker_list():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
- else:
- global server
- try:
- response = requests.post(server + "/upload/refresh-interlockers")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text.replace(",", "\n"))
+ data = {'interlocker-name': interlocker_name})
+ if (response.status_code == 200):
+ click.echo(f"Interlocker {interlocker_name} deleted.")
+ elif (response.status_code == 400 or
+ response.status_code == 404 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -469,57 +517,60 @@ def driver():
@click.command(help="Grab a train")
@click.argument('train')
@click.option('--engine', '-e', help="The train engine behaviour to use",
- default="libtrain_engine_default (unremovable)")
+ default="libtrain_engine_default (unremovable)")
def grab(train, engine):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global session_id, grab_id, server
if (session_id == 0 and grab_id == -1):
try:
response = requests.post(server + "/driver/grab-train",
data = {'train': train, 'engine': engine})
- if (response.status_code == requests.codes.ok):
- ids = response.text.split(",")
- update_session_and_grab_id(int(ids[0]), int(ids[1]))
- click.echo("Grabbed train " + train + "\n")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ update_session_and_grab_id(response_json["session-id"], response_json["grab-id"])
+ click.echo("Grabbed train " + train)
+ click.echo(f" Debug information: session-id ({response_json["session-id"]})")
+ click.echo(f" Debug information: grab-id ({response_json["grab-id"]})")
+ elif (response.status_code == 400 or
+ response.status_code == 404 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ log_common_feedback_err_msg(response.text, internal_err=True)
else:
- click.echo("System not running or train not available!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
else:
- click.echo("You can only grab one train!")
- return
-
-
-def process_id_feedback(feedback, success_message):
- if (feedback == "invalid session id"):
- update_session_and_grab_id(0, -1)
- click.echo("Session id has become invalid\n", err=True)
- elif (feedback == "invalid grab id"):
- update_session_and_grab_id(0, -1)
- click.echo("Grab id was invalid\n", err=True)
- else:
- click.echo(success_message)
+ click.echo("You already have a train grabbed! "
+ "Release it before grabbing a new train.\n"
+ "If your grab-id or session-id is not valid, i.e., if you do not actually "
+ "have control over a train, then please re-run the "
+ "reset_grab_and_session_id command.",
+ err=True)
@click.command(help="Release your train")
def release():
global session_id, grab_id, server
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
try:
response = requests.post(server + "/driver/release-train",
- data = {'session-id': session_id, 'grab-id': grab_id})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Released grabbed train\n")
+ data = {'session-id': session_id, 'grab-id': grab_id})
+ if (response.status_code == 200):
+ click.echo("Released grabbed train.")
update_session_and_grab_id(0, -1)
+ elif (response.status_code == 400 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -529,46 +580,50 @@ def release():
@click.argument('destination')
def request_route(source, destination):
global session_id, grab_id, server
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
try:
response = requests.post(server + "/driver/request-route",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'source': source, 'destination': destination})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Route " + response.text +
- " from " + source + " to " + destination + " has been granted\n")
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'source': source, 'destination': destination})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ route_id = response_json["granted-route-id"]
+ click.echo(f"Route {route_id} from {source} to {destination} has been granted.")
+ elif (response.status_code == 400 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("Route from " + source + " to " + destination +
- " has not been granted,\n" +
- "or system not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Request a specific route ID for your train ")
+@click.command(help="Request a specific route by its ID for your train")
@click.argument('route-id')
def request_route_id(route_id):
global session_id, grab_id, server
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
try:
- response = requests.post(server + "/driver/request-route-id",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'route-id': route_id})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Route " + route_id + " has been granted\n")
+ response = requests.post(server + "/driver/request-route-by-id",
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'route-id': route_id})
+ if (response.status_code == 200):
+ click.echo(f"Route {route_id} has been granted.")
+ elif (response.status_code == 400 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ log_common_feedback_err_msg(response.text, internal_err=True)
else:
- click.echo("Route " + route_id + " has not been granted,\n" +
- "or system not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -578,56 +633,61 @@ def request_route_id(route_id):
@click.argument('route-id')
def direction(train, route_id):
global server
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
try:
response = requests.post(server + "/driver/direction",
- data = {'session-id': session_id, 'train': train,
- 'route-id': route_id})
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ data = {'session-id': session_id, 'train': train,
+ 'route-id': route_id})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(f"Direction: {response_json["direction"]}.")
+ elif (response.status_code == 400 or
+ response.status_code == 404):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Drive along a granted route in automatic or manual mode")
-@click.argument('mode')
+@click.argument('mode', type=click.Choice(['manual', 'automatic']))
@click.argument('route-id')
def drive_route(mode, route_id):
global session_id, grab_id, server
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
+ elif ((not mode == "manual") and (not mode == "automatic")):
+ click.echo("Incorrect mode! Mode shall be either manual or automatic.")
else:
try:
response = requests.post(server + "/driver/drive-route",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'mode': mode, 'route-id': route_id})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Driving along route " + route_id +
- " completed\n")
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'mode': mode, 'route-id': route_id})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(f"Driving route {route_id} completed, server msg: {response_json.get("msg", "")}")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("Could not drive along route " + route_id + ",\n" +
- "or system not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Set the dcc speed step of your train")
-@click.option('--backwards', '-b', help="The rear end of the train moves forward",
- is_flag=True)
-@click.option('--track_output', '-t', help="Use another track output than the "
- "default one", default="")
+@click.option('--backwards', '-b', help="The rear end of the train moves forward", is_flag=True)
+@click.option('--track_output', '-t', help="Use another track output than the default one",
+ default="")
@click.argument('speed', type=click.IntRange(0, 126))
def set_dcc_speed(backwards, speed, track_output):
global session_id, grab_id, server, default_track_output
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
@@ -637,28 +697,27 @@ def set_dcc_speed(backwards, speed, track_output):
track_output = default_track_output
try:
response = requests.post(server + "/driver/set-dcc-train-speed",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'speed': speed, 'track-output': track_output})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "DCC train speed set to " +
- str(speed) + "\n")
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'speed': speed, 'track-output': track_output})
+ if (response.status_code == 200):
+ click.echo(f"DCC train speed set to {speed} on track-output {track_output}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Set the calibrated speed step of your train")
-@click.option('--backwards', '-b', help="The rear end of the train moves forward",
- is_flag=True)
-@click.option('--track_output', '-t', help="Use another track output than the "
- "default one", default="")
+@click.option('--backwards', '-b', help="The rear end of the train moves forward", is_flag=True)
+@click.option('--track_output', '-t', help="Use another track output than the default one",
+ default="")
@click.argument('speed', type=click.IntRange(0, 9))
def set_calibrated_speed(backwards, speed, track_output):
global session_id, grab_id, server, default_track_output
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
@@ -668,25 +727,25 @@ def set_calibrated_speed(backwards, speed, track_output):
track_output = default_track_output
try:
response = requests.post(server + "/driver/set-calibrated-train-speed",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'speed': speed, 'track-output': track_output})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Calibrated train speed set "
- "to " + str(speed) + "\n")
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'speed': speed, 'track-output': track_output})
+ if (response.status_code == 200):
+ click.echo(f"Calibrated train speed set to {speed} on track-output {track_output}.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Emergency stop your train")
-@click.option('--track_output', '-t', help="Use another track output than the "
- "default one", default="")
+@click.option('--track_output', '-t', help="Use another track output than the default one",
+ default="")
def emergency_stop(track_output):
global session_id, grab_id, server, default_track_output
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
@@ -694,26 +753,27 @@ def emergency_stop(track_output):
track_output = default_track_output
try:
response = requests.post(server + "/driver/set-train-emergency-stop",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'track-output': track_output})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Train emergency stopped\n")
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'track-output': track_output})
+ if (response.status_code == 200):
+ click.echo("Train emergency stopped.")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running or invalid track output!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Set a peripheral of your train")
-@click.option('--track_output', '-t', help="Use another track output than the "
- "default one", default="")
+@click.option('--track_output', '-t', help="Use another track output than the default one",
+ default="")
@click.argument('peripheral')
@click.argument('state', type=click.Choice(['off', 'on']))
def set_train_peripheral(peripheral, state, track_output):
global session_id, grab_id, server, default_track_output
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
elif (session_id == 0 and grab_id == -1):
click.echo("No train grabbed!")
else:
@@ -725,34 +785,46 @@ def set_train_peripheral(peripheral, state, track_output):
'on': 1,
}
response = requests.post(server + "/driver/set-train-peripheral",
- data = {'session-id': session_id, 'grab-id': grab_id,
- 'peripheral': peripheral, 'state': mapping.get(state),
- 'track-output': track_output})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Train peripheral "
- + peripheral + " set to " + state + "\n")
+ data = {'session-id': session_id, 'grab-id': grab_id,
+ 'peripheral': peripheral, 'state': mapping.get(state),
+ 'track-output': track_output})
+ if (response.status_code == 200):
+ click.echo(f"Train peripheral {peripheral} set to {state}")
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("System not running, invalid peripheral or invalid "
- "track output!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Upload a train engine. Absolute path to an SCCharts (*.sctx) file")
+@click.command(help="Upload a train engine")
@click.argument('filepath', type=click.Path(exists=True))
def upload_engine(filepath):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/upload/engine",
- files = {'file': open(filepath, 'r')})
- if (response.status_code == requests.codes.ok):
- process_id_feedback(response.text, "Train engine uploaded\n")
+ files = {'file': open(filepath, 'r')})
+ if (response.status_code == 200):
+ click.echo("Train engine uploaded.")
+ elif (response.status_code == 400):
+ response_json: dict = json.loads(response.text)
+ click.echo("Error, server msg: " + response_json.get("msg", ""), err=True)
+ # If the verification failed, the feedback can be quite long.
+ if (response_json.get("status", True) == False):
+ # easiest for now to directly print the whole verifiedproperties object
+ click.echo("Verification of at least one property failed. "
+ "Now printing info on all properties:")
+ click.echo(json.dumps(response_json["verifiedproperties"], indent=4, default=str))
+ elif (response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ log_common_feedback_err_msg(response.text, internal_err=True)
else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -760,35 +832,21 @@ def upload_engine(filepath):
@click.command(help="Delete a train engine")
@click.argument('engine-name')
def delete_engine(engine_name):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/upload/remove-engine",
- data = {'engine-name': engine_name})
- if (response.status_code == requests.codes.ok):
- click.echo("Train engine " + engine_name + " deleted\n")
+ data = {'engine-name': engine_name})
+ if (response.status_code == 200):
+ click.echo(f"Train engine {engine_name} deleted.")
+ elif (response.status_code == 400 or
+ response.status_code == 404 or
+ response.status_code == 409):
+ log_common_feedback_err_msg(response.text)
else:
- click.echo("Train engine still in use, or system not running or invalid state!\n",
- err=True)
- except requests.exceptions.RequestException as e:
- click.echo(e, err=True)
-
-
-@click.command(help="Get a list of train engines")
-def get_engine_list():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
- else:
- global server
- try:
- response = requests.post(server + "/upload/refresh-engines")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text.replace(",", "\n"))
- else:
- click.echo("System not running or invalid state!\n",
- err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -801,34 +859,37 @@ def monitor():
@click.command(help="Get the name of the platform/railway that is being run")
def get_platform_name():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.get(server + "/monitor/platform-name")
- if (response.status_code == requests.codes.ok):
- click.echo("Platform name: " + response.text)
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(f"Platform name: {response_json["platform-name"]}")
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(help="Get a list of trains")
def get_trains():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/trains")
- if (response.status_code == requests.codes.ok):
+ response = requests.get(server + "/monitor/trains")
+ if (response.status_code == 200):
if (response.text == ""):
- click.echo("No trains available")
+ click.echo("No trains available.")
else:
- click.echo(response.text)
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -836,17 +897,43 @@ def get_trains():
@click.command(help="Get the state of a train")
@click.argument('train')
def get_train_state(train):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/monitor/train-state",
- data = {'train': train})
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ data = {'train': train})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 404):
+ click.echo(f"Error, train {train} was not found.", err=True)
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running or train unknown!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
+ except requests.exceptions.RequestException as e:
+ click.echo(e, err=True)
+
+
+@click.command(help="Get the state of all trains")
+def get_trains_states():
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
+ else:
+ global server
+ try:
+ response = requests.get(server + "/monitor/train-states")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
+ else:
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -854,65 +941,161 @@ def get_train_state(train):
@click.command(help="Get a list of peripherals of a train")
@click.argument('train')
def get_train_peripherals(train):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.post(server + "/monitor/train-peripherals",
- data = {'train': train})
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ data = {'train': train})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running or train unknown!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Get a list of track outputs")
+@click.command(help="Get a list of train engines")
+def get_engine_list():
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
+ else:
+ global server
+ try:
+ response = requests.get(server + "/monitor/engines")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
+ else:
+ log_not_running_or_unknown_err(response.status_code)
+ except requests.exceptions.RequestException as e:
+ click.echo(e, err=True)
+
+
+@click.command(help="Get a list of interlockers")
+def get_interlocker_list():
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
+ else:
+ global server
+ try:
+ response = requests.get(server + "/monitor/interlockers")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
+ else:
+ log_not_running_or_unknown_err(response.status_code)
+ except requests.exceptions.RequestException as e:
+ click.echo(e, err=True)
+
+
+@click.command(help="Get a list of track outputs with their state")
def get_track_outputs():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/track-outputs")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/track-outputs")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get a list of points")
def get_points():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/points")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/points")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get a list of signals")
def get_signals():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/signals")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/signals")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
+ except requests.exceptions.RequestException as e:
+ click.echo(e, err=True)
+
+
+@click.command(help="Get detailed information on a point")
+@click.argument('point')
+def get_point_details(point):
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
+ else:
+ global server
+ try:
+ response = requests.post(server + "/monitor/point-details", data = {'point': point})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
+ else:
+ log_not_running_or_unknown_err(response.status_code)
+ except requests.exceptions.RequestException as e:
+ click.echo(e, err=True)
+
+
+@click.command(help="Get detailed information on a signal")
+@click.argument('signal')
+def get_signal_details(signal):
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
+ else:
+ global server
+ try:
+ response = requests.post(server + "/monitor/signal-details", data = {'signal': signal})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
+ else:
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -920,17 +1103,23 @@ def get_signals():
@click.command(help="Get the aspects of a point")
@click.argument('point')
def get_point_aspects(point):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/point-aspects",
- data = {'point': point})
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.post(server + "/monitor/point-aspects", data = {'point': point})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 404):
+ click.echo(f"Error, point {point} was not found.")
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running or point unknown!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -938,110 +1127,134 @@ def get_point_aspects(point):
@click.command(help="Get the aspects of a signal")
@click.argument('signal')
def get_signal_aspects(signal):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/signal-aspects",
- data = {'signal': signal})
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.post(server + "/monitor/signal-aspects", data = {'signal': signal})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 404):
+ click.echo(f"Error, signal {signal} was not found.")
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running or signal unknown!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get a list of segments")
def get_segments():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/segments")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/segments")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get a list of reversers")
def get_reversers():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/reversers")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/reversers")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message "
+ "or unable to get reverser state update.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get a list of peripherals")
def get_peripherals():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/peripherals")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/peripherals")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
-@click.command(help="Get whether plugins will be verified upon upload")
+
+@click.command(help="Get whether train engines will be verified upon upload")
def get_verification_option():
- if(parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.get(server + "/monitor/verification-option")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text + "\n")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
else:
- click.echo("System not running!\n", err=True)
+ click.echo(f"Unknown error, reply status code: {response.status_code}", err=True)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(help="Get the url to the verification server")
def get_verification_url():
- if(parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
response = requests.get(server + "/monitor/verification-url")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text + "\n")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
else:
- click.echo("System not running!\n", err=True)
+ click.echo(f"Unknown error, reply status code: {response.status_code}", err=True)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(help="Get a list of granted routes")
def get_granted_routes():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/granted-routes")
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.get(server + "/monitor/granted-routes")
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -1049,48 +1262,55 @@ def get_granted_routes():
@click.command(help="Get the details of a route")
@click.argument('route-id')
def get_route(route_id):
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/route",
- data = {'route-id': route_id})
- if (response.status_code == requests.codes.ok):
- click.echo(response.text)
+ response = requests.post(server + "/monitor/route", data = {'route-id': route_id})
+ if (response.status_code == 200):
+ response_json: dict = json.loads(response.text)
+ click.echo(json.dumps(response_json, indent=4, default=str))
+ elif (response.status_code == 400):
+ log_common_feedback_err_msg(response.text)
+ elif (response.status_code == 404):
+ click.echo(f"Route with ID {route_id} was not found.", err=True)
+ elif (response.status_code == 500):
+ click.echo("Internal error, server was unable to build a reply message.", err=True)
else:
- click.echo("System not running or route unknown!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@click.command(help="Get debug information")
def get_debug_info():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/debug")
- if (response.status_code == requests.codes.ok):
+ response = requests.get(server + "/monitor/debug")
+ if (response.status_code == 200):
click.echo(response.text)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
+
@click.command(help="Get extra debug information")
def get_debug_info_extra():
- if (parse_config()):
- click.echo("Corrupt config, please run the config command", err=True)
+ if (not parse_config()):
+ click.echo("Corrupt config, please run the config command.", err=True)
else:
global server
try:
- response = requests.post(server + "/monitor/debug_extra")
- if (response.status_code == requests.codes.ok):
+ response = requests.get(server + "/monitor/debug-extra")
+ if (response.status_code == 200):
click.echo(response.text)
else:
- click.echo("System not running!\n", err=True)
+ log_not_running_or_unknown_err(response.status_code)
except requests.exceptions.RequestException as e:
click.echo(e, err=True)
@@ -1098,6 +1318,7 @@ def get_debug_info_extra():
# --- assign commands to groups ---
cli.add_command(config)
+cli.add_command(reset_grab_and_session_id)
cli.add_command(admin)
cli.add_command(controller)
cli.add_command(driver)
@@ -1120,7 +1341,7 @@ controller.add_command(set_interlocker)
controller.add_command(unset_interlocker)
controller.add_command(upload_interlocker)
controller.add_command(delete_interlocker)
-controller.add_command(get_interlocker_list)
+
driver.add_command(grab)
driver.add_command(release)
@@ -1134,15 +1355,19 @@ driver.add_command(emergency_stop)
driver.add_command(set_train_peripheral)
driver.add_command(upload_engine)
driver.add_command(delete_engine)
-driver.add_command(get_engine_list)
monitor.add_command(get_platform_name)
monitor.add_command(get_trains)
monitor.add_command(get_train_state)
+monitor.add_command(get_trains_states)
monitor.add_command(get_train_peripherals)
+monitor.add_command(get_engine_list)
+monitor.add_command(get_interlocker_list)
monitor.add_command(get_track_outputs)
monitor.add_command(get_points)
monitor.add_command(get_signals)
+monitor.add_command(get_point_details)
+monitor.add_command(get_signal_details)
monitor.add_command(get_point_aspects)
monitor.add_command(get_signal_aspects)
monitor.add_command(get_segments)
diff --git a/configurations/swtbahn-full/bidib_board_config.yml b/configurations/swtbahn-full/bidib_board_config.yml
index 6e308102..fc96f4a6 100644
--- a/configurations/swtbahn-full/bidib_board_config.yml
+++ b/configurations/swtbahn-full/bidib_board_config.yml
@@ -21,6 +21,9 @@ boards:
unique-id: 0x45000D6B0031EF
- id: slave
unique-id: 0x42000D6700EAEB
+ features:
+ - number: 0x03
+ value: 0x14
- id: onecontrol3
unique-id: 0x45000D8D00BDF1
- id: onecontrol4
diff --git a/configurations/swtbahn-full/config.bahn b/configurations/swtbahn-full/config.bahn
index 2bd26a9e..10e20c46 100644
--- a/configurations/swtbahn-full/config.bahn
+++ b/configurations/swtbahn-full/config.bahn
@@ -4,7 +4,7 @@ module SWTbahnFull
# Long partition
master 0xDA000D6800B4F0
features
- 0x03:0x14 # seckack on, 200ms
+ 0x03:0x14 # secack on, 200ms
0x6E:0x00 # track output default off
end
onecontrol1 0x45000D8D00C1F1
@@ -16,6 +16,9 @@ module SWTbahnFull
# Short partition
slave 0x42000D6700EAEB
+ features
+ 0x03:0x14 # secack on, 200ms
+ end
onecontrol3 0x45000D8D00BDF1
onecontrol4 0x45000D8D0003F2
lightcontrol5 0x45000D6B002AEF
@@ -162,7 +165,7 @@ module SWTbahnFull
end
reversers master
- reverser 30051 block14
+ reverser 30051 block14
end
signals lightcontrol1
diff --git a/server/doc/api/API-Readme.md b/server/doc/api/API-Readme.md
new file mode 100644
index 00000000..960b47d4
--- /dev/null
+++ b/server/doc/api/API-Readme.md
@@ -0,0 +1,8 @@
+# Notes Regarding API & API Usage
+- Always analyze the returned HTTP status code first. Information about success or failure of a request to an endpoint is encoded in the HTTP status code of the reply.
+
+# Notes Regarding OpenAPI
+- For tools, specs etc. (also regarding the OpenAPI file here, `openapi_swtbahn_V`), see:
+ - [API spec version 3.0.0](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md)
+ - [OpenAPI website](https://www.openapis.org/)
+ - [Tool used to edit the spec file](https://mermade.github.io/openapi-gui/#) (see also the [github repo](https://github.com/mermade/openapi-gui))
\ No newline at end of file
diff --git a/server/doc/api/openapi_swtbahn_V1.json b/server/doc/api/openapi_swtbahn_V1.json
new file mode 100644
index 00000000..9d11add0
--- /dev/null
+++ b/server/doc/api/openapi_swtbahn_V1.json
@@ -0,0 +1,3207 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "SWTbahn",
+ "version": "1.0.0",
+ "contact": {},
+ "license": {
+ "name": "GPL-3.0"
+ },
+ "description": "SWTbahn server offering an API for interacting with an SWTbahn model railway."
+ },
+ "paths": {
+ "/admin/startup": {
+ "post": {
+ "summary": "Startup the SWTbahn",
+ "description": "Attempts to start up the SWTbahn to get it running; i.e., initialize connection to hardware, parse config files, and so on. This starts the server session if successful.",
+ "parameters": [],
+ "operationId": "admin-startup",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "SWTbahn is already running",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "SWTbahn unable to startup due to internal error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ }
+ },
+ "security": []
+ }
+ },
+ "/admin/shutdown": {
+ "post": {
+ "summary": "Shutdown the SWTbahn",
+ "description": "Attempts to shut down the SWTbahn (if it is running); i.e., close connection to hardware, stop running internal threads, and so on. This stops the current server session if successful.",
+ "parameters": [],
+ "operationId": "admin-shutdown",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": []
+ }
+ },
+ "/admin/set-track-output": {
+ "post": {
+ "summary": "Set the track output state",
+ "description": "Set the track output state for all track outputs",
+ "parameters": [],
+ "operationId": "admin-set-track-output",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_state"
+ }
+ }
+ },
+ "description": "the state that all track outputs shall be set to"
+ },
+ "security": []
+ }
+ },
+ "/admin/set-verification-option": {
+ "post": {
+ "summary": "Set the verification option",
+ "description": "Set the verification option to true or false, i.e., enable or disable the verification of train engines when they are uploaded. Note that verification will only work if a verification url to the swtbahn-verifier (or equivalent) server has been set.",
+ "parameters": [],
+ "operationId": "admin-set-verification-option",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_verification-option"
+ }
+ }
+ },
+ "description": "the new value of the verification option (true or false)"
+ },
+ "security": []
+ }
+ },
+ "/admin/set-verification-url": {
+ "post": {
+ "summary": "Set the verification url",
+ "description": "Set the verification url, i.e., the path for the websocket connection to the verification server for train engines.",
+ "parameters": [],
+ "operationId": "admin-set-verification-url",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_verification-url"
+ }
+ }
+ },
+ "description": "the new value of the verification url (should start with ws://)"
+ },
+ "security": []
+ }
+ },
+ "/admin/release-train": {
+ "post": {
+ "summary": "Release a train",
+ "description": "Force-release a train that is currently grabbed by someone.",
+ "parameters": [],
+ "operationId": "admin-release-train",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid train, or train not currently grabbed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_train"
+ }
+ }
+ },
+ "description": "the train that shall be released"
+ },
+ "security": []
+ }
+ },
+ "/admin/set-dcc-train-speed": {
+ "post": {
+ "summary": "Set the dcc speed of a train",
+ "description": "Set the dcc speed of a train. This is set directly via bidib, i.e., it does not use the train engine dynamic container. Note that this may lead to inconsistencies/the train changing its speed to something else after this command due to a change in the output of the train engine dynamic container.",
+ "parameters": [],
+ "operationId": "admin-set-dcc-train-speed",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid train, invalid speed, or invalid track output",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_train_speed_track-output"
+ }
+ }
+ },
+ "description": "the train whose speed shall be set, the speed in the range [-126, 126] , and the track output (usually 'master')."
+ },
+ "security": []
+ }
+ },
+ "/controller/release-route": {
+ "post": {
+ "summary": "releases a route",
+ "description": "releases the specified route that is currently granted to a train",
+ "parameters": [],
+ "operationId": "controller-release-route",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_route-id"
+ }
+ }
+ },
+ "description": "route which shall be released."
+ },
+ "security": []
+ }
+ },
+ "/controller/set-point": {
+ "post": {
+ "summary": "set the state/aspect of a point",
+ "description": "",
+ "parameters": [],
+ "operationId": "controller-set-point",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown Point",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_point_state"
+ }
+ }
+ },
+ "description": "point whose state shall be set, and state to set."
+ },
+ "security": []
+ }
+ },
+ "/controller/set-signal": {
+ "post": {
+ "summary": "set the state/aspect of a signal",
+ "description": "",
+ "parameters": [],
+ "operationId": "controller-set-signal",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown signal",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_signal_state"
+ }
+ }
+ },
+ "description": "signal whose state shall be set, and state to set."
+ },
+ "security": []
+ }
+ },
+ "/controller/set-peripheral": {
+ "post": {
+ "summary": "set the state/aspect of a peripheral",
+ "description": "",
+ "parameters": [],
+ "operationId": "controller-set-peripheral",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_peripheral_state"
+ }
+ }
+ },
+ "description": "peripheral whose state shall be set, and state to set."
+ },
+ "security": []
+ }
+ },
+ "/controller/set-interlocker": {
+ "post": {
+ "summary": "set the interlocker to be used by the SWTbahn",
+ "description": "",
+ "parameters": [],
+ "operationId": "controller-set-interlocker",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter or no more free interlocker instances available",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Another interlocker is already set",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_interlocker"
+ }
+ }
+ },
+ "description": "ID of the interlocker to be used by the SWTbahn."
+ },
+ "security": []
+ }
+ },
+ "/controller/unset-interlocker": {
+ "post": {
+ "summary": "unset the interlocker currently used by the SWTbahn",
+ "description": "",
+ "parameters": [],
+ "operationId": "controller-unset-interlocker",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "no interlocker currently set, or currently set interlocker has a different name",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_interlocker"
+ }
+ }
+ },
+ "description": "ID of the interlocker currently used by the SWTbahn that shall be unset."
+ },
+ "security": []
+ }
+ },
+ "/controller/get-interlocker": {
+ "get": {
+ "summary": "get the ID of the interlocker currently used by the SWTbahn",
+ "description": "",
+ "parameters": [],
+ "operationId": "controller-get-interlocker",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_get-interlocker"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "no interlocker currently set",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/driver/grab-train": {
+ "post": {
+ "summary": "grab a train",
+ "description": "Grab a train (to take temporary ownership). If successful, a grab-id (that identifies this temporary ownership) and a session-id (server session ID to detect server restarts which invalidate train ownerships) are returned. The ownership lasts until it is released (by an admin or by the driver), or until the server session ends.",
+ "parameters": [],
+ "operationId": "driver-grab-train",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_grab-train"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown train or invalid train state",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Train already grabbed or max. no of grabbed trains reached",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_train_engine"
+ }
+ }
+ },
+ "description": "A train engine ID has to be specified along with the ID of the train to grab.\nThe train engine determines how the train reacts to speed and direction requests/commands sent via the appropriate API endpoint.\nThe normal train engine is called 'libtrain_engine_default (unremovable)'."
+ }
+ }
+ },
+ "/driver/release-train": {
+ "post": {
+ "summary": "release a train",
+ "description": "Release a train that was grabbed with a specific grab-id in the current server session.",
+ "parameters": [],
+ "operationId": "driver-release-train",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Train is not currently grabbed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id"
+ }
+ }
+ },
+ "description": "The grab-id has to be specified which identifies the train ownership that is to be released.\nThe session-id has to be specified to make sure this train ownership was granted in the same server session."
+ }
+ }
+ },
+ "/driver/request-route": {
+ "post": {
+ "summary": "request a route",
+ "description": "Request a route from a source signal to a destination signal, to be driven with the grabbed train. If successful, a route is granted and its ID is returned.",
+ "parameters": [],
+ "operationId": "driver-request-route",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_request-route"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s), or other reason for no route being granted",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Route to be granted is in conflict with other granted route or in conflict with state of the track",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_source_destination"
+ }
+ }
+ },
+ "description": "The grab-id has to be specified which identifies the train ownership, i.e., the train with which the route is to be driven.\nThe session-id has to be specified to make sure the train ownership was granted in the same server session.\nThe source (destination) signal is the start (end) of the desired route. In case of composite signals (two signals on one post/mast), usually the specific ID of the non-distant signal has to be provided."
+ }
+ }
+ },
+ "/driver/request-route-by-id": {
+ "post": {
+ "summary": "request a route by its ID",
+ "description": "Request the route with a specific route-id, to be driven with the grabbed train. If successful, the route with the specified ID is granted.",
+ "parameters": [],
+ "operationId": "driver-request-route-by-id",
+ "responses": {
+ "200": {
+ "description": "Success (route is granted)"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s), or other reason for no route being granted",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Route to be granted is in conflict with other granted route or in conflict with state of the track, or route is already granted",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_route-id"
+ }
+ }
+ },
+ "description": "The grab-id has to be specified which identifies the train ownership, i.e., the train with which the route is to be driven.\nThe session-id has to be specified to make sure the train ownership was granted in the same server session.\nThe route-id identifies which route is being requested."
+ }
+ }
+ },
+ "/driver/direction": {
+ "post": {
+ "summary": "get the direction for driving a route",
+ "description": "Get the direction that the specified train would need to drive (forwards or backwards) to drive along the specified route.",
+ "parameters": [],
+ "operationId": "driver-direction",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_direction"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown route",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_train_route-id"
+ }
+ }
+ },
+ "description": "The train for which the direction is to be determined and the route-id of the route for which the driving direction is to be determined."
+ }
+ }
+ },
+ "/driver/drive-route": {
+ "post": {
+ "summary": "drive a route",
+ "description": "Drive the specified route with the grabbed train, either in manual mode (speed controlled by driver via separate set-speed commands) or in automatic mode (train is driven automatically along the route).\nNote that this request is blocking until the route is released (either manually through appropriate command or automatically when end of the route is reached), or an internal error occurs.\nThis only works if the route is currently granted to the grabbed train.",
+ "parameters": [],
+ "operationId": "driver-drive-route",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_route-id_mode"
+ }
+ }
+ },
+ "description": "The session-id of the current server session. \nThe grab-id that identifies the ownership of the train with which the route is driven.\nThe route-id that identifies the route to drive.\nThe mode that specifies whether driving is manual or automatic."
+ }
+ }
+ },
+ "/driver/set-dcc-train-speed": {
+ "post": {
+ "summary": "set the dcc speed of a train",
+ "description": "Set the requested speed (and thus also direction) of the grabbed train to a valid dcc speed. The speed request is passed to the train engine dynamic container.",
+ "parameters": [],
+ "operationId": "driver-set-dcc-train-speed",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_speed_track-output"
+ }
+ }
+ },
+ "description": "The session-id of the current server session. \nThe grab-id that identifies the ownership of the train whose speed is to be set.\nThe dcc speed to set.\nThe track output for which the train shall be set to the given speed (usually 'master')."
+ }
+ }
+ },
+ "/driver/set-calibrated-train-speed": {
+ "post": {
+ "summary": "set the speed of a train to a calibrated level",
+ "description": "Sets the speed of the grabbed train to a calibrated level (levels defined in config files)(and thus also direction). In contrast to driver-set-dcc-train-speed, this bypasses the train engine dynamic container, i.e., the speed is set directly via bidib. Note that this may lead to inconsistencies/the train changing its speed to something else after this command due to a change in the output of the train engine dynamic container.",
+ "parameters": [],
+ "operationId": "driver-set-calibrated-train-speed",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_speed_track-output__for-calibrated-speed"
+ }
+ }
+ },
+ "description": "The session-id of the current server session. \nThe grab-id that identifies the ownership of the train whose speed is to be set.\nThe calibrated speed level to set.\nThe track output for which the train shall be set to the given speed (usually 'master')."
+ }
+ }
+ },
+ "/driver/set-train-emergency-stop": {
+ "post": {
+ "summary": "set a train to emergency stop",
+ "description": "Emergency stops the grabbed train. This bypasses the train engine dynamic container, i.e., the emergency stop is set directly via bidib. Note that this may lead to inconsistencies/the train starting to drive again after the emergency stop due to a change in the output of the train engine dynamic container.",
+ "parameters": [],
+ "operationId": "driver-set-train-emergency-stop",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_track-output"
+ }
+ }
+ },
+ "description": "The session-id of the current server session. \nThe grab-id that identifies the ownership of the train which is to be stopped.\nThe track output for which the train shall be stopped (usually 'master')."
+ }
+ }
+ },
+ "/driver/set-train-peripheral": {
+ "post": {
+ "summary": "set the state of a train peripheral",
+ "description": "Set the state of the peripheral of the grabbed train",
+ "parameters": [],
+ "operationId": "driver-set-train-peripheral",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter(s)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_session-id_grab-id_peripheral_state_track-output"
+ }
+ }
+ },
+ "description": "The session-id of the current server session. \nThe grab-id that identifies the ownership of the train whose peripheral is to be set.\nThe peripheral whose state to set.\nThe state to set the peripheral to (string with '0' or '1' for on or off respectively).\nThe track output for which the train's peripheral shall be set (usually 'master')."
+ }
+ }
+ },
+ "/monitor/platform-name": {
+ "get": {
+ "summary": "get the name of the SWTbahn (platform) currently being operated",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-platform-name",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_platform-name"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/trains": {
+ "get": {
+ "summary": "get info of trains",
+ "description": "Gets information (ID, grab status, on-track status) of trains, for all trains defined in the configuration files of the SWTbahn platform being operated",
+ "parameters": [],
+ "operationId": "monitor-trains",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_trains"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/train-state": {
+ "post": {
+ "summary": "get info on state of specific train",
+ "description": "Gets information on state of a specific train",
+ "parameters": [],
+ "operationId": "monitor-train-state",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_train-state"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown train"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_train"
+ }
+ }
+ },
+ "description": "The name/ID of the train to get info on its state"
+ }
+ }
+ },
+ "/monitor/train-states": {
+ "get": {
+ "summary": "get info on state of all trains",
+ "description": "Gets information on state of all trains",
+ "parameters": [],
+ "operationId": "monitor-train-states",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_train-states"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/train-peripherals": {
+ "post": {
+ "summary": "get info on peripherals of a specific train",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-train-peripherals",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_train-peripherals"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown train"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_train"
+ }
+ }
+ },
+ "description": "The name/ID of the train whose peripherals to get info on"
+ }
+ }
+ },
+ "/monitor/engines": {
+ "get": {
+ "summary": "get names of available train engines (behavior models)",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-engines",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_engines"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/interlockers": {
+ "get": {
+ "summary": "get names of available interlockers",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-interlockers",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_interlockers"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/track-outputs": {
+ "get": {
+ "summary": "get names of available track ouputs",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-track-outputs",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_track-outputs"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/points": {
+ "get": {
+ "summary": "get info on points",
+ "description": "Get information (ID, state, and optionally whether the current target state is reached) on all points",
+ "parameters": [],
+ "operationId": "monitor-points",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_points"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/signals": {
+ "get": {
+ "summary": "get info on signals",
+ "description": "Get information (ID, state) on all signals",
+ "parameters": [],
+ "operationId": "monitor-signals",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_signals"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/point-details": {
+ "post": {
+ "summary": "get detailed info on a specific point",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-point-details",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_point-details"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown point"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_point"
+ }
+ }
+ },
+ "description": "The name/ID of the point"
+ }
+ }
+ },
+ "/monitor/signal-details": {
+ "post": {
+ "summary": "get detailed info on a specific signal - NOT YET IMPLEMENTED",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-signal-details",
+ "responses": {
+ "501": {
+ "description": "NOT YET IMPLEMENTED"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_signal"
+ }
+ }
+ },
+ "description": "The name/ID of the signal"
+ }
+ }
+ },
+ "/monitor/point-aspects": {
+ "post": {
+ "summary": "get info on aspects (states) a specific point supports",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-point-aspects",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_accessory-aspects"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown point"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_point"
+ }
+ }
+ },
+ "description": "The name/ID of the point"
+ }
+ }
+ },
+ "/monitor/signal-aspects": {
+ "post": {
+ "summary": "get info on aspects (states) a specific signal supports",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-signal-aspects",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_accessory-aspects"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown signal"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_signal"
+ }
+ }
+ },
+ "description": "The name/ID of the signal"
+ }
+ }
+ },
+ "/monitor/segments": {
+ "get": {
+ "summary": "get info on segments",
+ "description": "Get info on segments (ID, and, if occupied, who/what is occupying them)",
+ "parameters": [],
+ "operationId": "monitor-segments",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_segments"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/reversers": {
+ "get": {
+ "summary": "get info on reversers",
+ "description": "Get info on reversers (ID, state)",
+ "parameters": [],
+ "operationId": "monitor-reversers",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_reversers"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message or unable to get reverser state update"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/peripherals": {
+ "get": {
+ "summary": "get info on peripherals",
+ "description": "Get info on peripherals (ID, state ID, state value)",
+ "parameters": [],
+ "operationId": "monitor-peripherals",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_peripherals"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/verification-option": {
+ "get": {
+ "summary": "get the current verification option setting",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-verification-option",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_verification-option"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/verification-url": {
+ "get": {
+ "summary": "get the current verification url",
+ "description": "",
+ "parameters": [],
+ "operationId": "monitor-verification-url",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_verification-url"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/granted-routes": {
+ "get": {
+ "summary": "get info on granted routes",
+ "description": "Get info on granted routes, with ID of route and the train (ID) they are granted to",
+ "parameters": [],
+ "operationId": "monitor-granted-routes",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_granted-routes"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {}
+ }
+ },
+ "/monitor/route": {
+ "post": {
+ "summary": "get info on a specific route",
+ "description": "Get (detailed) info on a specific route, including conflicting route IDs etc.; reply can be quite big",
+ "parameters": [],
+ "operationId": "monitor-route",
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_route"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Unknown route"
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "500": {
+ "description": "Server unable to build reply message or other internal error"
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_route-id"
+ }
+ }
+ },
+ "description": "The name/ID of the route"
+ }
+ }
+ },
+ "/upload/engine": {
+ "post": {
+ "summary": "upload a train engine (behavior) model",
+ "description": "Upload a train engine behavior model in form of an .sctx (SCCharts) file.",
+ "parameters": [],
+ "operationId": "upload-engine",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter, or verification of train engine failed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/reply_engine__for-upload"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Engine with same name exists or no engine (container) slot is available",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Engine could not be compiled to a shared library",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/param_file"
+ }
+ }
+ },
+ "description": "The .sctx file containing the train engine."
+ }
+ }
+ },
+ "/upload/remove-engine": {
+ "post": {
+ "summary": "remove a train engine (behavior) model",
+ "description": "Remove a train engine (behavior) model from those that are available on the server. Note that the default engines which are not uploaded by users are not removable.",
+ "parameters": [],
+ "operationId": "upload-remove-engine",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter, or engine is unremovable",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Engine to remove not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Engine is still being used",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_engine-name"
+ }
+ }
+ },
+ "description": "The name of the train engine to remove."
+ }
+ }
+ },
+ "/upload/interlocker": {
+ "post": {
+ "summary": "upload an interlocker",
+ "description": "Upload a interlocker in form of a .bahn (BahnDSL) file.",
+ "parameters": [],
+ "operationId": "upload-interlocker",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Interlocker with same name exists or no interlocker (container) slot is available",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Interlocker could not be compiled to a shared library",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "multipart/form-data": {
+ "schema": {
+ "$ref": "#/components/schemas/param_file"
+ }
+ }
+ },
+ "description": "The .bahn file containing the interlocker."
+ }
+ }
+ },
+ "/upload/remove-interlocker": {
+ "post": {
+ "summary": "remove an interlocker",
+ "description": "Remove an interlocker from those that are available on the server. Note that the default interlockers which are not uploaded by users are not removable.",
+ "parameters": [],
+ "operationId": "upload-remove-interlocker",
+ "responses": {
+ "200": {
+ "description": "Success"
+ },
+ "400": {
+ "description": "Invalid or missing parameter, or interlocker is unremovable",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Interlocker to remove not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Method not allowed"
+ },
+ "409": {
+ "description": "Interlocker is still being used",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/common_feedback"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "SWTbahn not running"
+ }
+ },
+ "security": [],
+ "callbacks": {},
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/param_interlocker-name"
+ }
+ }
+ },
+ "description": "The name of the interlocker to remove."
+ }
+ }
+ }
+ },
+ "security": [],
+ "servers": [],
+ "components": {
+ "links": {},
+ "callbacks": {},
+ "schemas": {
+ "common_feedback": {
+ "title": "common_feedback",
+ "description": "simple object with feedback message",
+ "type": "object",
+ "properties": {
+ "msg": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_train": {
+ "title": "param_train",
+ "type": "object",
+ "properties": {
+ "train": {
+ "description": "name of the train",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_verification-url": {
+ "title": "param_verification-url",
+ "type": "object",
+ "properties": {
+ "verification-url": {
+ "description": "verification server url incl. port",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_verification-option": {
+ "title": "param_verification-option",
+ "type": "object",
+ "properties": {
+ "verification-option": {
+ "description": "verification option value string (value should be 'true' or 'false')",
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^(true|false)$"
+ }
+ }
+ },
+ "param_state": {
+ "title": "param_state",
+ "type": "object",
+ "properties": {
+ "state": {
+ "description": "state",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_route-id": {
+ "title": "param_route-id",
+ "type": "object",
+ "properties": {
+ "route-id": {
+ "description": "ID of the route",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_point_state": {
+ "title": "param_point_state",
+ "type": "object",
+ "properties": {
+ "point": {
+ "description": "ID of a point",
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "description": "desired point state",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_signal_state": {
+ "title": "param_signal_state",
+ "type": "object",
+ "properties": {
+ "signal": {
+ "description": "ID of a signal",
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "description": "desired signal state",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_peripheral_state": {
+ "title": "param_peripheral_state",
+ "type": "object",
+ "properties": {
+ "peripheral": {
+ "description": "ID of a peripheral",
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "description": "desired peripheral state",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_interlocker": {
+ "title": "param_interlocker",
+ "type": "object",
+ "properties": {
+ "interlocker": {
+ "description": "ID of an interlocker",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_get-interlocker": {
+ "title": "reply_get-interlocker",
+ "description": "interlocker",
+ "type": "object",
+ "properties": {
+ "interlocker": {
+ "description": "the ID of the interlocker that is currently set",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_train_engine": {
+ "title": "param_train_engine",
+ "type": "object",
+ "properties": {
+ "train": {
+ "description": "name of a train",
+ "type": "string",
+ "minLength": 1
+ },
+ "engine": {
+ "description": "name of a (train) engine",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_session-id_grab-id": {
+ "title": "param_session-id_grab-id",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_grab-train": {
+ "title": "reply_grab-train",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "type": "integer",
+ "description": "identifier for the current server session",
+ "minimum": 1
+ },
+ "grab-id": {
+ "type": "integer",
+ "description": "identifier for this grabbing/ownership of a train",
+ "minimum": 0
+ }
+ }
+ },
+ "param_session-id_grab-id_source_destination": {
+ "title": "param_session-id_grab-id_source_destination",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "source": {
+ "description": "name of the source signal",
+ "type": "string",
+ "minLength": 1
+ },
+ "destination": {
+ "description": "name of the destination signal",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_request-route": {
+ "title": "reply_request-route",
+ "type": "object",
+ "properties": {
+ "granted-route-id": {
+ "description": "ID of the granted route",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_session-id_grab-id_route-id": {
+ "title": "param_session-id_grab-id_route-id",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "route-id": {
+ "description": "ID of the route",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_train_route-id": {
+ "title": "param_train_route-id",
+ "type": "object",
+ "properties": {
+ "train": {
+ "description": "name of a train",
+ "type": "string",
+ "minLength": 1
+ },
+ "route-id": {
+ "description": "ID of the route that shall be considered for driving direction",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_direction": {
+ "title": "reply_direction",
+ "type": "object",
+ "properties": {
+ "direction": {
+ "type": "string",
+ "pattern": "^(forwards|backwards)$",
+ "description": "direction of train considering its location and route to be driven"
+ }
+ }
+ },
+ "param_session-id_grab-id_route-id_mode": {
+ "title": "param_session-id_grab-id_route-id_mode",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "route-id": {
+ "description": "ID of the route",
+ "type": "string",
+ "minLength": 1
+ },
+ "mode": {
+ "type": "string",
+ "pattern": "^(manual|automatic)$",
+ "description": "driving mode for the route, either manual or automatic"
+ }
+ }
+ },
+ "param_session-id_grab-id_speed_track-output": {
+ "title": "param_session-id_grab-id_speed_track-output",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "speed": {
+ "description": "speed to set the train to, in range [-126,126]",
+ "type": "string",
+ "minLength": 1
+ },
+ "track-output": {
+ "description": "the name of the track output node (usually master)",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_train_speed_track-output": {
+ "title": "param_train_speed_track-output",
+ "type": "object",
+ "properties": {
+ "train": {
+ "description": "name of a train",
+ "type": "string",
+ "minLength": 1
+ },
+ "speed": {
+ "description": "speed to set the train to, in range [-126,126]",
+ "type": "string",
+ "minLength": 1
+ },
+ "track-output": {
+ "description": "the name of the track output node (usually master)",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_session-id_grab-id_speed_track-output__for-calibrated-speed": {
+ "title": "param_session-id_grab-id_speed_track-output__for-calibrated-speed",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "speed": {
+ "description": "speed to set the train to, in range [-9,9], depending on the calibration levels configured for the grabbed train",
+ "type": "string",
+ "minLength": 1
+ },
+ "track-output": {
+ "description": "the name of the track output node (usually master)",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_session-id_grab-id_track-output": {
+ "title": "param_session-id_grab-id_track-output",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "track-output": {
+ "description": "the name of the track output node (usually master)",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_session-id_grab-id_peripheral_state_track-output": {
+ "title": "param_session-id_grab-id_peripheral_state_track-output",
+ "type": "object",
+ "properties": {
+ "session-id": {
+ "description": "session-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "grab-id": {
+ "description": "grab-id",
+ "type": "string",
+ "minLength": 1
+ },
+ "peripheral": {
+ "description": "ID of the train peripheral",
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "description": "desired peripheral state (either 0 (off) or 1 (on))",
+ "type": "string",
+ "minLength": 1
+ },
+ "track-output": {
+ "description": "the name of the track output node (usually master)",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_signal": {
+ "title": "param_signal",
+ "type": "object",
+ "properties": {
+ "signal": {
+ "description": "ID of a signal",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_point": {
+ "title": "param_point",
+ "type": "object",
+ "properties": {
+ "point": {
+ "description": "ID of a point",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_route": {
+ "title": "reply_route",
+ "description": "Info on a specific route",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "ID of the route",
+ "minLength": 1
+ },
+ "source_signal": {
+ "type": "string",
+ "minLength": 1
+ },
+ "destination_signal": {
+ "type": "string",
+ "minLength": 1
+ },
+ "orientation": {
+ "type": "string",
+ "pattern": "^(anticlockwise|clockwise)$"
+ },
+ "length": {
+ "type": "number",
+ "description": "length, usually in centimeters (depends on config)"
+ },
+ "path": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": false,
+ "description": "path of the route, in order of intended traversal, including signals that are passed",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "sections": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "signals": {
+ "type": "array",
+ "minItems": 2,
+ "description": "Includes at least source and destination signals, plus any passed non-distant signals",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "points": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "conflicting_route_ids": {
+ "type": "array",
+ "minItems": 0,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "granted_conflicting_route_ids": {
+ "type": "array",
+ "minItems": 0,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "clear": {
+ "type": "boolean"
+ },
+ "granted_to_train": {
+ "type": "string",
+ "minLength": 0,
+ "description": "Contains name of the train if the route is currently granted, otherwise empty"
+ }
+ }
+ },
+ "reply_granted-routes": {
+ "title": "reply_granted-routes",
+ "description": "List of granted routes, each entry has the route ID and name of train which the route is granted to",
+ "type": "object",
+ "properties": {
+ "granted-routes": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "train": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_verification-url": {
+ "title": "reply_verification-url",
+ "description": "Verification Server URL",
+ "type": "object",
+ "properties": {
+ "verification-url": {
+ "type": "string",
+ "minLength": 0
+ }
+ }
+ },
+ "reply_verification-option": {
+ "title": "reply_verification-option",
+ "description": "State of verification option, i.e., whether it is enabled or disabled",
+ "type": "object",
+ "properties": {
+ "verification-enabled": {
+ "type": "boolean"
+ }
+ }
+ },
+ "reply_peripherals": {
+ "title": "reply_peripherals",
+ "description": "List of peripherals, with their identifier and info on their state id and state value",
+ "type": "object",
+ "properties": {
+ "peripherals": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "state-id": {
+ "type": "string",
+ "minLength": 0
+ },
+ "state-value": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_reversers": {
+ "title": "reply_reversers",
+ "description": "List of reversers of the platform with their state",
+ "type": "object",
+ "properties": {
+ "reversers": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "type": "string",
+ "pattern": "^(on|off|unknown)$"
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_segments": {
+ "title": "reply_segments",
+ "description": "List of segments, with info on occupancy per segment if it is occupied",
+ "type": "object",
+ "properties": {
+ "segments": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "occupied-by": {
+ "description": "list of occupiers (names of train(s) or 'unknown' if occupier is not identifiable)",
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_point-details": {
+ "title": "reply_point-details",
+ "description": "Detailed information on a point",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "aspects": {
+ "type": "array",
+ "minItems": 0,
+ "description": "list of aspects, i.e., states, that this point can be set to. currently allows answers with no aspects in the list, can occur if low-level querying of aspects fails",
+ "items": {
+ "type": "string"
+ }
+ },
+ "state": {
+ "type": "string",
+ "minLength": 0
+ },
+ "segment": {
+ "type": "string",
+ "minLength": 1
+ },
+ "occupied": {
+ "type": "boolean"
+ },
+ "target_state_reached": {
+ "type": "boolean",
+ "description": "This is optional, as DCC-accessories (so possibly points) don't provide this info. True if execution state is reached or reached_verified, otherwise false."
+ }
+ },
+ "required": [
+ "id",
+ "aspects",
+ "state",
+ "segment",
+ "occupied"
+ ]
+ },
+ "reply_signals": {
+ "title": "reply_signals",
+ "description": "List of signals, consisting of the signal ID and its state",
+ "type": "object",
+ "properties": {
+ "signals": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "type": "string",
+ "minLength": 0
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_points": {
+ "title": "reply_points",
+ "description": "List of points, consisting of the point ID, its state, and the target_state_reached status if available",
+ "type": "object",
+ "properties": {
+ "points": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "required": [
+ "id",
+ "state"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "type": "string",
+ "minLength": 0
+ },
+ "target_state_reached": {
+ "type": "boolean",
+ "description": "This is optional, as DCC-accessories (so possibly points) don't provide this info. True if execution state is reached or reached_verified, otherwise false."
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_track-outputs": {
+ "title": "reply_track-outputs",
+ "description": "List of track outputs with their identifier and their state",
+ "type": "object",
+ "properties": {
+ "track-outputs": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "type": "string",
+ "minLength": 0
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_interlockers": {
+ "title": "reply_interlockers",
+ "description": "List of interlocker identifiers",
+ "type": "object",
+ "properties": {
+ "interlockers": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "reply_engines": {
+ "title": "reply_engines",
+ "description": "List of train engine identifiers (i.e., those of available train behaviour models, not physical trains)",
+ "type": "object",
+ "properties": {
+ "engines": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "reply_train-peripherals": {
+ "title": "reply_train-peripherals",
+ "description": "List of peripherals of a train with their state",
+ "type": "object",
+ "properties": {
+ "train-peripherals": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "state": {
+ "type": "string",
+ "pattern": "^(on|off|unknown)$"
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_train-state": {
+ "title": "reply_train-state",
+ "description": "state of a train",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "grabbed": {
+ "type": "boolean"
+ },
+ "orientation": {
+ "type": "string",
+ "pattern": "^(left|right)$"
+ },
+ "direction": {
+ "type": "string",
+ "pattern": "^(forwards|backwards)$"
+ },
+ "speed_step": {
+ "type": "number"
+ },
+ "detected_kmh_speed": {
+ "type": "number",
+ "description": "Some trains don't report their km/h speed; in that case the value is 0 or this field is not present"
+ },
+ "route_id": {
+ "type": "string",
+ "description": "empty string or not present if no route is granted to this train. Otherwise route id.",
+ "minLength": 0
+ },
+ "on_track": {
+ "type": "boolean"
+ },
+ "occupied_segments": {
+ "type": "array",
+ "description": "occupied_segments is only included if on_track is true.",
+ "items": {
+ "type": "string"
+ }
+ },
+ "occupied_blocks": {
+ "type": "array",
+ "description": "occupied_blocks is only included if on_track is true.",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "id",
+ "grabbed",
+ "orientation",
+ "direction",
+ "speed_step",
+ "on_track"
+ ]
+ },
+ "reply_trains": {
+ "title": "reply_trains",
+ "description": "List of trains, each entry has train ID, grabbed status and on_track status",
+ "type": "object",
+ "properties": {
+ "trains": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1
+ },
+ "grabbed": {
+ "type": "boolean"
+ },
+ "on_track": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ },
+ "reply_platform-name": {
+ "title": "reply_platform-name",
+ "type": "object",
+ "properties": {
+ "platform-name": {
+ "type": "string",
+ "description": "name of the SWTbahn (platform) which is being operated",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_accessory-aspects": {
+ "title": "reply_accessory-aspects",
+ "description": "List of aspects (i.e., states) that a specific accessory (e.g., point, signal) can be set to",
+ "type": "object",
+ "properties": {
+ "aspects": {
+ "type": "array",
+ "minItems": 0,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ "param_file": {
+ "title": "param_file",
+ "type": "object",
+ "properties": {
+ "file": {
+ "description": "file. Attach via your request libraries form data, try with default format/encoding",
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ },
+ "reply_engine__for-upload": {
+ "title": "reply_engine__for-upload",
+ "description": "Result info of uploading an engine",
+ "type": "object",
+ "properties": {
+ "msg": {
+ "type": "string",
+ "description": "(error) message",
+ "minLength": 1
+ },
+ "status": {
+ "type": "boolean",
+ "description": "true if all properties of the engine model hold, false otherwise"
+ },
+ "__MESSAGE_TYPE__": {
+ "type": "string",
+ "description": "this can safely be ignored"
+ },
+ "verifiedproperties": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "property": {
+ "type": "object",
+ "properties": {
+ "formula": {
+ "type": "string",
+ "description": "formula of the property"
+ },
+ "name": {
+ "type": "string",
+ "description": "name of the property"
+ },
+ "type": {
+ "type": "string",
+ "description": "type of the property (ltl or invariant)"
+ }
+ }
+ },
+ "resultcode": {
+ "type": "string",
+ "pattern": "^(proven|disproven|unknown)$",
+ "description": "result of verification of this property"
+ },
+ "verificationlog": {
+ "type": "string",
+ "description": "Base64 encoded String. If decoded, this will give the verification log from nuXmv, including counterexamples at the end if possible. The string might also be compressed with zlib (uncompress after initial base64 decode), but default is uncompressed."
+ },
+ "verificationmessage": {
+ "type": "string",
+ "description": "Textual description of verification verdict for this property"
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "msg"
+ ]
+ },
+ "param_engine-name": {
+ "title": "param_engine-name",
+ "type": "object",
+ "properties": {
+ "engine-name": {
+ "description": "name of a (train) engine",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "param_interlocker-name": {
+ "title": "param_interlocker-name",
+ "type": "object",
+ "properties": {
+ "interlocker-name": {
+ "description": "name of an interlocker",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "reply_train-states": {
+ "title": "reply_train-states",
+ "description": "List of train states",
+ "type": "object",
+ "properties": {
+ "train-states": {
+ "type": "array",
+ "uniqueItems": true,
+ "minItems": 0,
+ "items": {
+ "$ref": "#/components/schemas/reply_train-state"
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/assets/game/script-game.js b/server/src/assets/game/script-game.js
index 0e0e0dba..38c47124 100644
--- a/server/src/assets/game/script-game.js
+++ b/server/src/assets/game/script-game.js
@@ -20,31 +20,27 @@ var signalFlagMap = null; // Global access variable for signal flag mapping
// If the server does reply with the platform name, tries to load the destinations and the flags
// mapped for the respective platform. If this fails, does not perform a retry.
function tryLoadDestinationsAndFlagMap() {
- // Try to get the platform name from the server, retry every 2 seconds on failure.
let retryDelay = 2000;
- fetch(serverAddress + '/monitor/platform-name',)
+ fetch(serverAddress + '/monitor/platform-name')
.then(response => {
if (response.status !== 200) {
console.log("Getting platform name failed: Retrying in %d ms", retryDelay);
setTimeout(() => tryLoadDestinationsAndFlagMap(), retryDelay);
} else {
- // response.text() gives a promise, not immediately a string, therefore pass
- // it to the next then block via return.
- return response.text();
+ return response.json();
}
})
- .then(responseText => {
- // Check if the previous then block actually returned something (i.e., if server
- // responded with a reply with code 200)
- if (responseText !== undefined) {
- console.log("Platform name received from server:", responseText);
- platform = responseText.toLowerCase();
- // Set the possible destinations and signal flag map for the SWTbahn platform.
+ .then(data => {
+ if (data !== undefined && data["platform-name"]) {
+ console.log("Platform name received from server:", data["platform-name"]);
+ let platform = data["platform-name"].toLowerCase();
allPossibleDestinations = eval("allPossibleDestinations_" + platform);
signalFlagMap = eval("signalFlagMap_" + platform);
}
})
- .catch(error => console.error("tryLoadDestinationsAndFlagMap (get platform name) err:", error));
+ .catch(error => {
+ console.error("tryLoadDestinationsAndFlagMap (get platform name) err:", error);
+ });
}
// Returns the destinations possible from a given block
@@ -55,13 +51,6 @@ function getDestinations(blockId) {
return allPossibleDestinations[blockId];
}
-// Destructures a route into its destination signal and route details
-function unpackRoute(route) {
- for (let destinationSignal in route) {
- return [destinationSignal, route[destinationSignal]];
- }
- return null;
-}
function disableAllDestinationButtons() {
driver.clearUpdatePossibleDestinationsInterval();
@@ -72,28 +61,23 @@ function disableAllDestinationButtons() {
}
}
-function setDestinationButton(choice, route) {
- const [destinationSignal, routeDetails] = unpackRoute(route);
- const destination = destinationSignal.replace(/(a|b)$/, '');
+function setDestinationButton(routeIndex, route, destinationSignal) {
+ // "Clean" the "a" or "b" suffix of composite signal if present
+ const destinationSigCleaned = destinationSignal.replace(/(a|b)$/, '');
// Route details are stored in the value parameter of the destination button
- $(`#${destinationNamePrefix}${choice}`).val(JSON.stringify(route));
- $(`#${destinationNamePrefix}${choice}`).attr("class", signalFlagMap[destination]);
+ $(`#${destinationNamePrefix}${routeIndex}`).val(JSON.stringify(route));
+ $(`#${destinationNamePrefix}${routeIndex}`).attr("class", signalFlagMap[destinationSigCleaned]);
}
-function setDestinationButtonAvailable(choice, route) {
- setDestinationButton(choice, route);
- $(`#${destinationNamePrefix}${choice}`).removeClass("flagThemeDisabled");
+function setDestinationButtonAvailable(routeIndex, route, destinationSignal) {
+ setDestinationButton(routeIndex, route, destinationSignal);
+ $(`#${destinationNamePrefix}${routeIndex}`).removeClass("flagThemeDisabled");
}
-function setDestinationButtonUnavailable(choice, route) {
- setDestinationButton(choice, route);
- $(`#${destinationNamePrefix}${choice}`).addClass("flagThemeDisabled");
-}
-
-function setChosenTrain(trainId) {
- const imageName = `train-${trainId.replace("_", "-")}.jpg`
- $("#chosenTrain").attr("src", imageName);
+function setDestinationButtonUnavailable(routeIndex, route, destinationSignal) {
+ setDestinationButton(routeIndex, route, destinationSignal);
+ $(`#${destinationNamePrefix}${routeIndex}`).addClass("flagThemeDisabled");
}
// Periodically update the availability of a blocks possible destinations.
@@ -105,66 +89,78 @@ function updatePossibleDestinations(blockId) {
disableAllDestinationButtons();
// Set up a timer interval to periodically update the availability
- const updatePossibleDestinationsTimeout = 1000;
+ const updatePossibleDestinationsTimeout = 1000; // 1000ms
driver.updatePossibleDestinationsInterval = setInterval(() => {
- console.log("Checking available destinations ...");
-
- const routes = getDestinations(blockId);
- if (routes == null) {
+ const routesFromCurrentBlock = getDestinations(blockId);
+ if (routesFromCurrentBlock == null) {
+ console.warn('updatePossibleDestinations: routesFromCurrentBlock is NULL');
return;
}
- Object.keys(routes).forEach((destinationSignal, choice) => {
- const route = { };
- route[destinationSignal] = routes[destinationSignal];
-
- const [_destinationSignal, routeDetails] = unpackRoute(route);
- let routeId = routeDetails["route-id"];
+ // Object.keys foreach
+ // -> routeIndex is the index of the route with destination signal `destinationSignal`
+ // in the collection `routesFromCurrentBlock`
+ Object.keys(routesFromCurrentBlock).forEach((destinationSignal, routeIndex) => {
+ let route = {};
+ route[destinationSignal] = routesFromCurrentBlock[destinationSignal];
+ route['destSignalID'] = destinationSignal;
+ ///TODO: check if this works (changed/simplified)
+ const routeId = route[destinationSignal]["route-id"];
updateDestinationAvailabilityPromise(
routeId,
// route is available
- () => setDestinationButtonAvailable(choice, route),
+ () => setDestinationButtonAvailable(routeIndex, route, destinationSignal),
// route is unavailable
- () => setDestinationButtonUnavailable(choice, route)
+ () => setDestinationButtonUnavailable(routeIndex, route, destinationSignal)
);
});
}, updatePossibleDestinationsTimeout);
}
-// Server request for a route's status and then determine whether the
-// route is available
+// Request for a route's status and then determine whether the route is available
function updateDestinationAvailabilityPromise(routeId, available, unavailable) {
return $.ajax({
type: 'POST',
url: serverAddress + '/monitor/route',
crossDomain: true,
- data: {
- 'route-id': routeId,
- },
+ data: { 'route-id': routeId },
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- const isNotConflicting = responseData.includes("granted conflicting route ids: none");
- const isRouteClear = responseData.includes("route clear: yes");
- const isNotGranted = responseData.includes("granted train: none");
+ success: (responseText) => {
+ const responseJson = JSON.parse(responseText);
+ const noConflicts = Array.isArray(responseJson.granted_conflicting_route_ids) &&
+ responseJson.granted_conflicting_route_ids.length === 0;
+ const isClear = responseJson.clear === true;
+ const isNotGranted = responseJson.granted_to_train === "";
+ const isAvailable = noConflicts && isClear && isNotGranted;
- const isAvailable = isNotConflicting && isRouteClear && isNotGranted;
if (isAvailable) {
available();
} else {
unavailable();
}
},
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse',
- '😢 There was a problem checking the destinations',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/monitor/route error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/monitor/route error with no message, status:", jqXHR.status);
+ }
+ unavailable();
+ setResponseDanger(
+ '#serverResponse',
+ '😢 There was a problem checking the destinations',
'😢 Es ist ein Problem beim Überprüfen der Ziele aufgetreten',
'Sorry'
);
}
});
-
}
+function setChosenTrain(trainId) {
+ const imageName = `train-${trainId.replace("_", "-")}.jpg`
+ $("#chosenTrain").attr("src", imageName);
+}
/**************************************************
* Train speed UI elements
@@ -223,41 +219,47 @@ function setResponse(responseId, messageEn, messageDe, callback) {
}
function speak(text) {
- var msg = new SpeechSynthesisUtterance(text);
+ if (!text) {
+ // Don't speak if text is not truthy, i.e., undefined, null, or empty.
+ return;
+ }
+ ///TODO: Check if this works, it was `var msg` before.
+ let speakMsg = new SpeechSynthesisUtterance(text);
+ //speakMsg.lang = (language == 'en') ? "en-US" : "de-DE";
+ ///NOTE: At the moment, all voice messages are in german or "german-ish" (like "Sorry")
+ speakMsg.lang = "de-DE";
for (const voice of window.speechSynthesis.getVoices()) {
if (voice.lang == "de-DE") {
- msg.voice = voice;
+ speakMsg.voice = voice;
break;
}
}
- window.speechSynthesis.speak(msg);
+ window.speechSynthesis.speak(speakMsg);
}
function setResponseDanger(responseId, messageEn, messageDe, messageSpeak) {
- setResponse(responseId, messageEn, messageDe, function() {
+ setResponse(responseId, messageEn, messageDe, function () {
$(responseId).parent().addClass('alert-danger');
$(responseId).parent().addClass('alert-danger-blink');
$(responseId).parent().removeClass('alert-success');
});
-
speak(messageSpeak);
}
function setResponseSuccess(responseId, messageEn, messageDe, messageSpeak) {
- setResponse(responseId, messageEn, messageDe, function() {
+ setResponse(responseId, messageEn, messageDe, function () {
$(responseId).parent().removeClass('alert-danger');
$(responseId).parent().removeClass('alert-danger-blink');
$(responseId).parent().addClass('alert-success');
});
-
speak(messageSpeak);
}
const modalMessages = {
drivingInfringement: {
title: {
- de: '👎 Fahrt nicht zulässig!',
- en: '👎 Driving Infringement!'
+ de: 'Unsichere Fahrt! 👎',
+ en: 'Driving Infringement! 👎'
},
body: {
de: 'Du hast deinen Zug nicht vor dem Zielsignal gestoppt!
Glücklicherweise konnten wir deinen Zug stoppen, bevor dieser mit einem anderen kollidieren oder die Schienen beschädigen konnte.',
@@ -274,8 +276,8 @@ const modalMessages = {
en: 'Continue Driving'
},
body: {
- de: 'Du hast dein Ziel noch nicht erreicht. Bitte fahr weiter 😀',
- en: 'You have not yet reached your destination. Please continue driving 😀'
+ de: 'Du hast dein Ziel noch nicht erreicht. Bitte fahr weiter.',
+ en: 'You have not yet reached your destination. Please continue driving.'
},
button: {
de: 'Verstanden',
@@ -288,8 +290,8 @@ const modalMessages = {
en: 'Destination Reached!'
},
body: {
- de: '🥳 Du hast deinen Zug zur deiner ausgewählten Station gefahren',
- en: '🥳 You drove your train to your chosen destination!'
+ de: 'Du hast deinen Zug zur deiner ausgewählten Station gefahren! 🥳',
+ en: 'You drove your train to your chosen destination! 🥳'
},
button: {
de: 'Super!',
@@ -309,7 +311,7 @@ function setModal(message) {
$('#serverModalTitle').text(title);
$('#serverModalBody').html(body);
$('#serverModalButton').text(button);
-
+
let modalElement = document.getElementById('serverModal');
let modal = bootstrap.Modal.getOrCreateInstance(modalElement);
modal.show();
@@ -318,14 +320,12 @@ function setModal(message) {
function setModalDanger(message, messageSpeak) {
setModal(message);
$('#serverModal .modal-content').addClass('modal-danger');
-
speak(messageSpeak);
}
function setModalSuccess(message, messageSpeak) {
setModal(message);
$('#serverModal .modal-content').addClass('modal-success');
-
speak(messageSpeak);
}
@@ -373,16 +373,16 @@ class Driver {
drivingTimer = new Timer();
}
- reset() {
+ resetTrainSession() {
this.sessionId = 0;
this.grabId = -1;
}
- get hasValidTrainSession() {
+ hasValidTrainSession() {
return (this.sessionId != 0 && this.grabId != -1)
}
- get hasRouteGranted() {
+ hasRouteGranted() {
return (this.routeDetails != null);
}
@@ -401,70 +401,86 @@ class Driver {
clearInterval(this.destinationReachedInterval);
}
- // Server request for the train's current block
+ // Request for the train's current block
updateCurrentBlockPromise() {
return $.ajax({
type: 'POST',
url: serverAddress + '/monitor/train-state',
crossDomain: true,
- data: {
- 'train': this.trainId
- },
+ data: { 'train': this.trainId },
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- const regexMatch = /on block: (.*?) /g.exec(responseData);
- this.currentBlock = regexMatch[1];
+ success: (responseText) => {
+ const responseJson = JSON.parse(responseText);
+ if (responseJson.on_track && Array.isArray(responseJson.occupied_blocks)
+ && responseJson.occupied_blocks.length > 0) {
+ this.currentBlock = responseJson.occupied_blocks[0];
+ }
},
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse',
- '😢 Could not find your train',
- '😢 Dein Zug konnte nicht gefunden werden',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/monitor/train-state error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/monitor/train-state error with no message, status:", jqXHR.status);
+ }
+ setResponseDanger('#serverResponse',
+ 'Could not find your train 😢',
+ 'Dein Zug konnte nicht gefunden werden 😢',
''
);
}
});
}
- // Server request for a train's status and then execute the callback handlers
- trainIsAvailablePromise(trainId, success, error) {
+
+ // Request for a train's status and then execute the callback handlers
+ trainIsAvailablePromise(trainId, successCallback, errorCallback) {
return $.ajax({
type: 'POST',
url: serverAddress + '/monitor/train-state',
crossDomain: true,
data: { 'train': trainId },
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- if (responseData.includes("grabbed: no") && !responseData.includes("on segment: no")) {
- success();
+ success: (responseText) => {
+ const responseJson = JSON.parse(responseText);
+ if (responseJson.grabbed === false && responseJson.on_track === true) {
+ successCallback();
} else {
- error();
+ errorCallback();
}
},
- error: (responseData, textStatus, errorThrown) => {
- // Do nothing
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/monitor/train-state error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/monitor/train-state error with no message, status:", jqXHR.status);
+ }
+ errorCallback();
}
});
}
+
// Update the styling of the train selection buttons based on the train availabilities
updateTrainAvailability() {
$('.selectTrainButton').prop("disabled", true);
-
+
const trainAvailabilityTimeout = 1000;
this.trainAvailabilityInterval = setInterval(() => {
- console.log("Checking available trains ... ");
// Enable a train if it is on the tracks and has not been grabbed
$('.selectTrainButton').each((index, obj) => {
let trainId = obj.id;
this.trainIsAvailablePromise(
trainId,
- () => {
+ // Success Callback
+ () => {
$(obj).prop("disabled", false);
$(obj).removeClass("btn-danger");
$(obj).addClass("btn-primary");
$($(obj).parent(".card-body").parent(".card")).removeClass("unavailableTrain");
-
+
function setVisibility(isShow) {
if (isShow) {
return "";
@@ -472,22 +488,23 @@ class Driver {
return "style='display: none;'";
}
}
- $(obj).children().each(function() {
+ $(obj).children().each(function () {
switch ($(this).attr('lang')) {
case "de": $(this).html(`Klicke um den Zug zu fahrenZug fahren`);
- break;
+ break;
case "en": $(this).html(`Click to drive this trainDrive this train`);
- break;
+ break;
}
});
},
+ // Error Callback
() => {
$(obj).prop("disabled", true);
$(obj).removeClass("btn-primary");
$(obj).addClass("btn-danger");
$($(obj).parent(".card-body").parent(".card")).addClass("unavailableTrain");
- $(obj).children().each(function() {
- switch($(this).attr('lang')){
+ $(obj).children().each(function () {
+ switch ($(this).attr('lang')) {
case "de": $(this).text("Nicht verfügbar"); break;
case "en": $(this).text("Unavailable"); break;
}
@@ -499,7 +516,7 @@ class Driver {
}
- // Server request to grab a train
+ // Request to grab a train
grabTrainPromise() {
return $.ajax({
type: 'POST',
@@ -510,24 +527,34 @@ class Driver {
'engine': this.trainEngine
},
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- const responseDataSplit = responseData.split(',');
- this.sessionId = responseDataSplit[0];
- this.grabId = responseDataSplit[1];
-
- setResponseSuccess('#serverResponse', '😁 Your train is ready', '😁 Dein Zug ist bereit', '');
+ success: (responseText) => {
+ const responseJson = JSON.parse(responseText);
+ this.sessionId = responseJson['session-id'];
+ this.grabId = responseJson['grab-id'];
+ setResponseSuccess('#serverResponse',
+ 'Your train is ready 😁',
+ 'Dein Zug ist bereit 😁',
+ ''
+ );
},
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse',
- '😢 There was a problem starting your train',
- '😢 Es ist ein Problem beim starten deines Zuges aufgetreten',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/driver/grab-train error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/driver/grab-train error with no message, status:", jqXHR.status);
+ }
+ setResponseDanger('#serverResponse',
+ 'There was a problem starting your train 😢',
+ 'Es ist ein Problem beim Starten deines Zuges aufgetreten 😢',
''
);
}
});
}
- // Server request for the train's physical driving direction of its granted route
+
+ // Request for the train's physical driving direction of its granted route
updateDrivingDirectionPromise() {
return $.ajax({
type: 'POST',
@@ -538,16 +565,28 @@ class Driver {
'route-id': this.routeDetails["route-id"]
},
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- this.drivingIsForwards = responseData.includes("forwards");
+ success: (responseText) => {
+ const responseJson = JSON.parse(responseText);
+ this.drivingIsForwards = responseJson.direction === "forwards";
},
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse', '😢 Could not find your train', '😢 Dein Zug konnte nicht gefunden werden', '');
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/driver/direction error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/driver/direction error with no message, status:", jqXHR.status);
+ }
+ setResponseDanger('#serverResponse',
+ 'Could not find your train 😢',
+ 'Dein Zug konnte nicht gefunden werden 😢',
+ ''
+ );
}
});
}
- // Server request to set the train's speed
+
+ // Request to set the train's speed
setTrainSpeedPromise(speed) {
return $.ajax({
type: 'POST',
@@ -560,17 +599,24 @@ class Driver {
'track-output': this.trackOutput
},
dataType: 'text',
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse',
- '😢 There was a problem setting the speed of your train',
- '😢 Es ist ein Problem beim Einstellen der Geschwindigkeit deines Zuges aufgetreten',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/driver/set-dcc-train-speed error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/driver/set-dcc-train-speed error with no message, status:", jqXHR.status);
+ }
+ setResponseDanger('#serverResponse',
+ 'There was a problem setting the speed of your train 😢',
+ 'Es ist ein Problem beim Einstellen der Geschwindigkeit aufgetreten 😢',
''
);
}
});
}
- // Server request for the train's current segment and then determine
+
+ // Request for the train's current segment and then determine
// whether to show the destination reached button
enableDestinationReachedPromise() {
const destinationReachedTimeout = 100;
@@ -579,44 +625,59 @@ class Driver {
type: 'POST',
url: serverAddress + '/monitor/train-state',
crossDomain: true,
- data: {
- 'train': this.trainId
- },
+ data: { 'train': this.trainId },
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- const matches = /on segment: (.*?) -/g.exec(responseData); // Get all Segment IDS as String
- const segmentIDs = matches[1];
- const segments = segmentIDs.split(", "); // Splits them into Array
-
- // Show the destination reached button when the train is only on the
- // main segment of the destination
- // Take into account that a main segment could be split into a/b segments
+ success: (responseText) => {
+ const responseJson = JSON.parse(responseText);
+ if (!(responseJson.on_track) || !Array.isArray(responseJson.occupied_segments)) {
+ return;
+ }
+
+ const segmentIDs = responseJson.occupied_segments;
+ const segments = segmentIDs.map(s => s.replace(/(a|b)$/, ''));
+
+ ///NOTE: Adjusted this to enable DestinationReached, when the train
+ // occupies the expected destination main segment (this.routeDetails['segment'])
+ // AND the train occupies 3 segments or less.
+ // Previously, DestinationReached would only be enabled if the train
+ // *exclusively* occupied the destination main segment. This was problematic
+ // for routes with a short main segment on the block at end of the route.
+ // Drivers were getting a warning for stopping, even though they were already
+ // close to the destination signal.
+ ///TODO: Test if this can be simplified by `&& this.routeDetails['segment'] in segments`
+ if (segments.length <= 3) {
+ for (let index in segments) {
+ if (segments[index] === this.routeDetails['segment']) {
+ this.clearDestinationReachedInterval();
+ this.isDestinationReached = true;
+ $('#endGameButton').show();
+ $(window).unbind('beforeunload', pageRefreshWarning);
+ return;
+ }
+ }
+ }
+ /* OLD VERSION - SEE NOTE ABOVE
for (let index in segments) {
- segments[index] = segments[index].replace(/(a|b)$/, '');
if (segments[index] != this.routeDetails['segment']) {
return;
}
}
-
- if(segments[0] == this.routeDetails['segment']) {
+ if (segments[0] == this.routeDetails['segment']) {
this.clearDestinationReachedInterval();
this.isDestinationReached = true;
$('#endGameButton').show();
-
- // The page can be refreshed without ill consequences.
- // The train will stop sensibly on the main segment of the destination
$(window).unbind('beforeunload', pageRefreshWarning);
- }
+ }*/
}
});
}, destinationReachedTimeout);
}
-
+
disableDestinationReached() {
this.isDestinationReached = false;
}
- // Server request to release the train
+ // Request to release the train
releaseTrainPromise() {
return $.ajax({
type: 'POST',
@@ -627,48 +688,65 @@ class Driver {
'grab-id': this.grabId
},
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- this.reset();
+ success: (_responseText) => {
+ console.log("Train was released: ", this.trainId);
+ this.resetTrainSession();
this.trainId = null;
},
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse',
- '🤔 There was a problem ending your turn',
- '🤔 Es ist ein Problem beim Beenden deiner Runde aufgetreten',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/driver/release-train error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/driver/release-train error with no message, status:", jqXHR.status);
+ }
+ setResponseDanger('#serverResponse',
+ 'There was a problem ending your turn 🤔',
+ 'Es ist ein Problem beim Beenden deiner Runde aufgetreten 🤔',
''
);
}
});
}
- // Server request for a specific route ID
- requestRouteIdPromise(routeDetails) {
+ // Request for a specific route ID
+ requestRouteIdPromise(pRouteDetails) {
return $.ajax({
type: 'POST',
- url: serverAddress + '/driver/request-route-id',
+ url: serverAddress + '/driver/request-route-by-id',
crossDomain: true,
data: {
'session-id': this.sessionId,
'grab-id': this.grabId,
- 'route-id': routeDetails['route-id']
+ 'route-id': pRouteDetails['route-id']
},
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- this.routeDetails = routeDetails;
- setResponseSuccess('#serverResponse',
- '🥳 Start driving your train to your chosen destination',
- '🥳 Fahr deinen Zug zum ausgewählten Ziel',
+ success: (_responseText) => {
+ this.routeDetails = pRouteDetails;
+ setResponseSuccess('#serverResponse',
+ 'Start driving your train to your chosen destination 🥳',
+ 'Fahr deinen Zug zum ausgewählten Ziel 🥳',
''
);
},
- error: (responseData, textStatus, errorThrown) => {
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/driver/request-route-by-id error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/driver/request-route-by-id error with no message, status:", jqXHR.status);
+ }
this.routeDetails = null;
- setResponseDanger('#serverResponse', "This route is not available", "Diese Route ist zurzeit leider nicht verfügbar", 'Sorry');
+ setResponseDanger('#serverResponse',
+ 'This route is currently not available',
+ 'Diese Route ist zurzeit leider nicht verfügbar',
+ 'Sorry'
+ );
}
});
}
- // Server request to manually drive the granted route
+ // Request to manually drive the granted route
driveRoutePromise() {
return $.ajax({
type: 'POST',
@@ -681,38 +759,60 @@ class Driver {
'mode': 'manual'
},
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- if (!this.hasValidTrainSession) {
- // Ignore, driver has ended their trip
- } else if (!this.hasRouteGranted) {
- setModalSuccess(modalMessages.drivingSuccess, 'Juhuu!!');
+ success: (_responseText) => {
+ if (!this.hasValidTrainSession()) {
+ // Driver has already released the train
+ console.log("driveRoutePromise called, success, but no valid train session.");
+ } else if (!this.hasRouteGranted()) {
+ // if (!this.hasRouteGranted()) is true when driveRoute returns from server,
+ // that means the *client* (aka the state in this driver object) has already
+ // released the route explicitly (which is done if the client clicks on "stop"
+ // whilst on the "destination segment"). Meaning, the driver *has* stopped
+ // the train, and driveRoute has now returned from the server -> this means
+ // that the driver has most likely stopped the train before the server had to
+ // stop it (server stops train if the destination signal is passed).
+ setModalSuccess(modalMessages.drivingSuccess, 'Juhuu!');
} else {
+ // driveRoute returns, and driver has valid train session, and the *client*
+ // (aka the state in this driver object) "thinks" that the route has NOT
+ // been released yet. Means, the driver has not pressed "stop" whilst on the
+ // "destination segment", otherwise the client would have released the route
+ // already. So, the server must have stopped the train when it passed the
+ // destination signal.
+ // Note that the server then automatically has already released the route,
+ // so calling that again is not necessary.
this.routeDetails = null;
-
- // Copy the driving infringement message and fill in the destination flag
+
const destinationClass = $('#destination').attr('class');
let drivingInfringement = JSON.parse(JSON.stringify(modalMessages.drivingInfringement));
drivingInfringement['body']['de'] = drivingInfringement['body']['de'].replace('${destination}', destinationClass);
drivingInfringement['body']['en'] = drivingInfringement['body']['en'].replace('${destination}', destinationClass);
- setModalDanger(drivingInfringement, 'STOP, STOP, STOP');
+ setModalDanger(drivingInfringement, 'Zielsignal verpasst!');
}
},
- error: (responseData, textStatus, errorThrown) => {
- setResponseDanger('#serverResponse',
- '😢 Route to your chosen destination is unavailable',
- '😢 Die Route zu deinem ausgewählten Ziel ist aktuell nicht verfügbar',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/driver/drive-route error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/driver/drive-route error with no message, status:", jqXHR.status);
+ }
+ setResponseDanger('#serverResponse',
+ 'Route to your chosen destination is currently unavailable 😢',
+ 'Die Route zu deinem ausgewählten Ziel ist aktuell nicht verfügbar 😢',
'Sorry'
);
}
});
}
- // Server request to release the granted route
+
+ // Request to release the granted route
releaseRoutePromise() {
- if (!this.hasRouteGranted) {
+ if (!this.hasRouteGranted()) {
return;
}
-
+
const routeId = this.routeDetails["route-id"];
this.routeDetails = null;
@@ -721,34 +821,50 @@ class Driver {
url: serverAddress + '/controller/release-route',
crossDomain: true,
data: { 'route-id': routeId },
- dataType: 'text'
+ dataType: 'text',
+ error: (jqXHR) => {
+ if ('msg' in jqXHR.responseText) {
+ const responseJson = JSON.parse(responseText);
+ console.warn("/controller/release-route error with returned message:", responseJson.msg);
+ } else {
+ console.warn("/controller/release-route error with no message, status:", jqXHR.status);
+ }
+ }
});
}
+
// Manage the business logic of manually driving a granted route
async driveToPromise(route) {
- if (!this.hasValidTrainSession) {
- setResponseDanger('#serverResponse', '😢 Could not find your train', '😢 Dein Zug konnte nicht gefunden werden', '');
+ if (!this.hasValidTrainSession()) {
+ setResponseDanger('#serverResponse',
+ 'Could not find your train 😢',
+ 'Dein Zug konnte nicht gefunden werden 😢',
+ ''
+ );
return;
}
-
+
const lock = await Mutex.lock();
-
- if (this.hasRouteGranted) {
+
+ if (this.hasRouteGranted()) {
+ // If train already has a route, return.
Mutex.unlock(lock);
return;
}
- const [destinationSignal, routeDetails] = unpackRoute(route);
- console.log(routeDetails);
-
- this.requestRouteIdPromise(routeDetails) // 1. Ensure that the chosen destination is still available
+ const destinationSignal = route['destSignalID'];
+ const pRouteDetails = route[destinationSignal];
+ console.log("driveToPromise: driving a route to signal: ", destinationSignal);
+
+ // Note: this.routeDetails is set to pRouteDetails in this.requestRouteIdPromise success handler!
+ this.requestRouteIdPromise(pRouteDetails) // 1. Ensure that the chosen destination is still available
.then(() => this.updateDrivingDirectionPromise()) // 2. Obtain the physical driving direction
.then(() => $('#destinationsForm').hide()) // 3. Prevent the driver from choosing another destination
.then(() => disableAllDestinationButtons())
.then(() => Mutex.unlock(lock))
.then(() => this.setTrainSpeedPromise(1)) // 4. Update the train lights to indicate the physical driving direction
- .then(() => wait(1))
+ .then(() => wait(1)) // 1ms
.then(() => this.setTrainSpeedPromise(0))
.then(() => setChosenDestination(destinationSignal)) // 5. Show the chosen destination and possible train speeds to the driver
.then(() => enableSpeedButtons(destinationSignal))
@@ -760,7 +876,7 @@ class Driver {
.then(() => clearChosenDestination())
.then(() => {
- if (!this.hasValidTrainSession) { // 9. Check whether the driver has quit their session
+ if (!this.hasValidTrainSession()) { // 9. Check whether the driver has quit their session
this.clearDestinationReachedInterval();
throw new Error("Driver ended their session");
}
@@ -784,12 +900,16 @@ function startGameLogic() {
// FIXME: On iOS, speech synthesis only works if it is first triggered by the user.
speak("");
- if (driver.hasValidTrainSession) {
- setResponseDanger('#serverResponse', 'You are already driving a train!', 'Du fährst aktuell schon einen Zug!', 'STOP!')
+ if (driver.hasValidTrainSession()) {
+ setResponseDanger('#serverResponse',
+ 'You are already driving a train!',
+ 'Du fährst aktuell schon einen Zug!',
+ ''
+ );
return;
}
- setResponseSuccess('#serverResponse', '⏳ Waiting ...', '⏳ Warten ...');
+ setResponseSuccess('#serverResponse', '⏳ Waiting ...', '⏳ Warten ...', '');
driver.grabTrainPromise()
.then(() => $('#trainSelection').hide())
@@ -806,7 +926,7 @@ function endGameLogic() {
$('#destinationsForm').hide();
driver.clearUpdatePossibleDestinationsInterval();
driver.clearDestinationReachedInterval();
- driver.reset();
+ driver.resetTrainSession();
disableAllDestinationButtons();
driver.disableDestinationReached();
disableSpeedButtons();
@@ -852,7 +972,7 @@ function initialise() {
language = 'de';
$('span:lang(de):not(span span)').show();
$('span:lang(en):not(span span)').hide();
-
+
// Handle language selection.
$('#changeLang').click(function () {
language = (language == 'en') ? 'de' : 'en';
@@ -864,7 +984,7 @@ function initialise() {
isEasyMode = false;
$('.normal').show();
$('.easy').hide();
-
+
// Handle the verbosity selection.
$('#changeTips').click(function (event) {
isEasyMode = !isEasyMode
@@ -897,7 +1017,11 @@ function initialise() {
// Should check if button is enabled here
if (destinationButton.hasClass("flagThemeDisabled")) {
console.log("Route Driving was not attempted because it is disabled");
- setResponseDanger('#serverResponse', "This route is not available", "Diese Route ist zurzeit leider nicht verfügbar", 'Sorry');
+ setResponseDanger('#serverResponse',
+ 'This route is currently not available',
+ 'Diese Route ist zurzeit leider nicht verfügbar',
+ ''
+ );
} else {
const route = JSON.parse(destinationButton.val());
driver.driveToPromise(route);
@@ -907,7 +1031,9 @@ function initialise() {
disableSpeedButtons();
- // Initialise the click handler of each speed button.
+ // Initialise the click handler of each speed button.
+ // `speed` here matching the button IDs, as you can see in the definition of `speedButtons`
+ // and the HTML file.
speedButtons.forEach(speed => {
const speedButton = $(`#${speed}`);
speedButton.click(function () {
@@ -916,6 +1042,8 @@ function initialise() {
$('#endGameButton').hide();
if (speedButton.val() == '0') {
+ // Driver has stopped the train, but has not reached the destination segment yet.
+ // -> tell the driver to continue driving.
setModal(modalMessages.drivingContinue);
}
@@ -923,28 +1051,33 @@ function initialise() {
// The train might not stop sensibly on the main segment of the destination
$(window).bind("beforeunload", pageRefreshWarning);
} else if (speedButton.val() == '0') {
+ // Destination is reached and driver stops the train -> release route.
driver.releaseRoutePromise();
}
});
});
$('#endGameButton').click(function () {
- if (!driver.hasValidTrainSession) {
+ if (!driver.hasValidTrainSession()) {
endGameLogic();
return;
}
- setResponseSuccess('#serverResponse', '⏳ Waiting ...', '⏳ Warten ...');
+ setResponseSuccess('#serverResponse', 'Waiting... ⏳', 'Warten...⏳', '');
driver.setTrainSpeedPromise(0)
- .then(() => wait(500))
+ .then(() => wait(500)) // 500 ms
.always(() => {
driver.releaseTrainPromise();
driver.releaseRoutePromise();
endGameLogic();
});
- setResponseSuccess('#serverResponse', '😀 Thank you for playing', '😀 Danke fürs Spielen', 'Danke fürs Spielen');
+ setResponseSuccess('#serverResponse',
+ 'Thank you for playing 😀',
+ 'Danke fürs Spielen 😀',
+ 'Danke fürs Spielen'
+ );
});
}
@@ -960,12 +1093,18 @@ $(document).ready(() => {
// Before page unload (refresh or close) behaviour
function pageRefreshWarning(event) {
event.preventDefault();
- console.log("Before unloading");
-
// Most web browsers will display a generic message instead!!
- const message = "Are you sure you want to refresh or leave this page? " +
- "Leaving this page without ending your game will prevent others from grabbing your train 😕";
- return event.returnValue = message;
+ let warnmessage = "";
+ if (language == 'de') {
+ warnmessage = "Bist du dir sicher, dass du die Seite neu laden oder verlassen willst? " +
+ "Das Verlassen oder neu Laden wird den Zug unverfügbar machen! " +
+ "Bitte nur dann durchführen, wenn du darum gebeten wirst.";
+ } else {
+ warnmessage = "Are you sure you want to refresh or leave this page? " +
+ "Leaving this page without ending your game will prevent others from grabbing your train. " +
+ "Please only do so if requested to.";
+ }
+ return event.returnValue = warnmessage;
}
// During page unload (refresh or close) behaviour
@@ -973,12 +1112,12 @@ function pageRefreshWarning(event) {
// Promises will not be executed!
$(window).on("unload", (event) => {
console.log("Unloading");
- if (driver.hasRouteGranted) {
+ if (driver.hasRouteGranted()) {
const formData = new FormData();
formData.append('route-id', driver.routeDetails['route-id']);
navigator.sendBeacon(serverAddress + '/controller/release-route', formData);
}
- if (driver.hasValidTrainSession) {
+ if (driver.hasValidTrainSession()) {
const formData = new FormData();
formData.append('session-id', driver.sessionId);
formData.append('grab-id', driver.grabId);
diff --git a/server/src/assets/script.js b/server/src/assets/script.js
index 917e0f83..f35a986a 100644
--- a/server/src/assets/script.js
+++ b/server/src/assets/script.js
@@ -7,6 +7,80 @@ var verificationObj = {};
var trainIsForwards = true;
var lastSetSpeed = 0; // Only needed when swapping train direction
+var customCode_500ServerUnableReplyMsg = {500: "Server unable to build reply message"};
+
+const speeds = [
+ "0",
+ "10",
+ "20",
+ "30",
+ "40",
+ "50",
+ "60",
+ "70",
+ "80",
+ "90",
+ "100",
+ "110",
+ "120",
+ "126"
+]
+
+// Helpers
+
+function showInfoInElem(elementID, textToShow) {
+ $(elementID).parent().removeClass('alert-danger');
+ $(elementID).parent().addClass('alert-success');
+ $(elementID).text(textToShow);
+}
+
+function showErrorInElem(elementID, textToShow) {
+ $(elementID).parent().removeClass('alert-success');
+ $(elementID).parent().addClass('alert-danger');
+ $(elementID).text(textToShow);
+}
+
+function getErrorMessage(jqXHR, customCodes = {}) {
+ try {
+ if (jqXHR.responseText.length > 0) {
+ const respJson = JSON.parse(jqXHR.responseText);
+ // If response has a "msg" field, that's the error msg (by convention in swtbahn-cli)
+ if (respJson.msg) {
+ return respJson.msg;
+ }
+ }
+ } catch (parseError) {
+ console.log("Unable to parse jqXHR to JSON.");
+ }
+
+ // Check if customCodes contains the status code
+ if (customCodes.hasOwnProperty(jqXHR.status)) {
+ return customCodes[jqXHR.status];
+ }
+
+ // Fallback to default messages
+ switch (jqXHR.status) {
+ case 405:
+ return "Method not allowed";
+ case 503:
+ return "SWTbahn not running";
+ default:
+ return `${jqXHR.status} - Error message not defined`;
+ }
+}
+
+//From https://github.com/eligrey/FileSaver.js/wiki/FileSaver.js-Example
+function SaveAsFile(content, filename, contentTypeOptions) {
+ try {
+ var b = new Blob([content], { type: contentTypeOptions });
+ saveAs(b, filename);
+ } catch (e) {
+ console.log("SaveAsFile Failed to save the file. Error msg: " + e);
+ }
+}
+
+// State Updates
+
function updateTrainIsForwards() {
$.ajax({
type: 'POST',
@@ -14,504 +88,716 @@ function updateTrainIsForwards() {
crossDomain: true,
data: { 'train': trainId },
dataType: 'text',
- success: function (responseData, textStatus, jqXHR) {
- trainIsForwards = responseData.includes('direction: forwards');
+ success: function (responseText) {
+ const responseJson = JSON.parse(responseText);
+ trainIsForwards = responseJson.direction === 'forwards';
}
});
}
function updateTrainGrabbedState() {
+ // Called from client.html - TODO: Test if the return is needed.
return $.ajax({
- type: 'POST',
+ type: 'GET',
url: '/monitor/trains',
crossDomain: true,
- data: null,
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
- const trains = responseData.split(/\r?\n|\r|\n/g);
- trains.forEach((train) => {
- const trainId = train.match(/^\w+_\w+/g)[0];
- const isGrabbed = train.includes('yes');
-
+ success: function (responseText) {
+ const responseJson = JSON.parse(responseText);
+ responseJson['trains'].forEach((train) => {
+ const trainId = train.id;
+ const isGrabbed = train.grabbed;
if (isGrabbed) {
$(`#releaseTrainButton_${trainId}`).show();
} else {
$(`#releaseTrainButton_${trainId}`).hide();
}
});
- },
- error: (responseData, textStatus, errorThrown) => {
- // Do nothing
}
});
}
function updateGrantedRoutes(htmlElement) {
return $.ajax({
- type: 'POST',
+ type: 'GET',
url: '/monitor/granted-routes',
crossDomain: true,
- data: null,
dataType: 'text',
- success: (responseData, textStatus, jqXHR) => {
+ success: function (responseText) {
+ const responseJson = JSON.parse(responseText);
htmlElement.empty();
-
- if (responseData.includes('No granted routes')) {
+ if (!responseJson['granted-routes'] || responseJson['granted-routes'].length === 0) {
htmlElement.html('