diff --git a/.gitignore b/.gitignore index e968fe6..8bf317a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,25 @@ webserver/build obj/ utils/dnp3_src/CMakeFiles/ webserver/scripts/ethercat +utils/ + +# Build artifacts +install_log.txt +start_openplc.sh +webserver/core/*.c +webserver/core/*.cpp +webserver/core/*.h +webserver/core/*.o +webserver/core/VARIABLES.csv +webserver/core/glue_generator +webserver/core/openplc +webserver/iec2c +webserver/st_optimizer +webserver/active_program +utils/libmodbus_src/src/.libs/* +utils/libmodbus_src/tests/.libs/* +webserver/scripts/openplc_driver +webserver/scripts/openplc_platform # Runtime directories -.venv +.venv \ No newline at end of file diff --git a/webserver/active_program b/webserver/active_program index 5621f10..9414382 100755 --- a/webserver/active_program +++ b/webserver/active_program @@ -1 +1 @@ -890590.st +Dockerfile diff --git a/webserver/openplc.py b/webserver/openplc.py index 6980c97..f1a22f7 100644 --- a/webserver/openplc.py +++ b/webserver/openplc.py @@ -114,9 +114,9 @@ class runtime: compilation_status_str = "" # Extract debug information from program - f = open('./st_files/' + st_file, "r") - combined_lines = f.read() - f.close() + with open('./st_files/' + st_file, "r") as f: + combined_lines = f.read() + combined_lines = combined_lines.split('\n') program_lines = [] c_debug_lines = [] @@ -132,9 +132,9 @@ class runtime: # Could not find debug info on program uploaded if os.path.isfile('./st_files/' + st_file + '.dbg'): # Debugger info exists on file - open it - f = open('./st_files/' + st_file + '.dbg', "r") - c_debug = f.read() - f.close() + with open('./st_files/' + st_file + '.dbg', "r") as f: + c_debug = f.read() + else: # No debug info... probably a program generated from the old editor. Use the blank debug info just to compile the program f = open('./core/debug.blank', "r") @@ -142,9 +142,8 @@ class runtime: f.close() # Write c_debug file - f = open('./core/debug.cpp', "w") - f.write(c_debug) - f.close() + with open('./core/debug.cpp', "w") as f: + f.write(c_debug) # Start compilation a = subprocess.Popen(['./scripts/compile_program.sh', str(st_file)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -155,17 +154,15 @@ class runtime: c_debug = '\n'.join(c_debug_lines) # Write c_debug file - f = open('./core/debug.cpp', "w") - f.write(c_debug) - f.close() + with open('./core/debug.cpp', "w") as f: + f.write(c_debug) #Write program and debug files - f = open('./st_files/' + st_file, "w") - f.write(program) - f.close() - f = open('./st_files/' + st_file + '.dbg', "w") - f.write(c_debug) - f.close() + with open('./st_files/' + st_file, "w") as f: + f.write(program) + + with open('./st_files/' + st_file + '.dbg', "w") as f: + f.write(c_debug) # Start compilation a = subprocess.Popen(['./scripts/compile_program.sh', str(st_file)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) diff --git a/webserver/restapi.py b/webserver/restapi.py new file mode 100644 index 0000000..339d7f3 --- /dev/null +++ b/webserver/restapi.py @@ -0,0 +1,51 @@ +# restblueprint.py +from flask import Blueprint, jsonify, request +from typing import Callable, Optional + +# Define the Blueprint +restapi_bp = Blueprint('restapi_blueprint', __name__) + +# Global variable to store the single callback for this blueprint +_handler_callback_get: Optional[Callable[[str, dict], dict]] = None +_handler_callback_post: Optional[Callable[[str, dict], dict]] = None + +def register_callback_get(callback: Callable[[str, dict], dict]): + """Registers the business logic callback function.""" + global _handler_callback_get + _handler_callback_get = callback + print("GET Callback registered successfully for rest_blueprint!") + +def register_callback_post(callback: Callable[[str, dict], dict]): + """Registers the business logic callback function.""" + global _handler_callback_post + _handler_callback_post = callback + print("POST Callback registered successfully for rest_blueprint!") + +@restapi_bp.route("/", methods=["GET"]) +def restapi_plc_get(command): + if _handler_callback_get is None: + return jsonify({"error": "No handler registered"}), 500 + + try: + data = request.args.to_dict() + + result = _handler_callback_get(command, data) + return jsonify(result), 200 + except Exception as e: + print(f"Error in restapi_plc_get: {e}") + return jsonify({"error": str(e)}), 500 + +@restapi_bp.route("/", methods=["POST"]) +def restapi_plc_post(command): + if _handler_callback_post is None: + return jsonify({"error": "No handler registered"}), 500 + + try: + # TODO validate file and limit size + data = request.get_json(silent=True) or {} + + result = _handler_callback_post(command, data) + return jsonify(result), 200 + except Exception as e: + print(f"Error in restapi_plc_post: {e}") + return jsonify({"error": str(e)}), 500 diff --git a/webserver/scripts/openplc_driver b/webserver/scripts/openplc_driver index 61aaae3..055bd00 100755 --- a/webserver/scripts/openplc_driver +++ b/webserver/scripts/openplc_driver @@ -1 +1 @@ -rpi +blank_linux diff --git a/webserver/scripts/openplc_platform b/webserver/scripts/openplc_platform index 61aaae3..a08e1f3 100755 --- a/webserver/scripts/openplc_platform +++ b/webserver/scripts/openplc_platform @@ -1 +1 @@ -rpi +linux diff --git a/webserver/st_files/Dockerfile b/webserver/st_files/Dockerfile new file mode 100644 index 0000000..a6d8cb4 --- /dev/null +++ b/webserver/st_files/Dockerfile @@ -0,0 +1,31 @@ +FROM debian:bullseye-20240722 + +COPY . /workdir + +WORKDIR /workdir + +RUN mkdir /docker_persistent + +VOLUME /docker_persistent + +# setup docker +RUN ./install.sh docker \ + && touch /docker_persistent/mbconfig.cfg \ + && touch /docker_persistent/persistent.file \ + && mkdir /docker_persistent/st_files \ + && cp /workdir/webserver/openplc.db /docker_persistent/openplc.db \ + && mv /workdir/webserver/openplc.db /workdir/webserver/openplc_default.db \ + && cp /workdir/webserver/dnp3.cfg /docker_persistent/dnp3.cfg \ + && mv /workdir/webserver/dnp3.cfg /workdir/webserver/dnp3_default.cfg \ + && cp -r /workdir/webserver/st_files/ /docker_persistent/st_files/ \ + && mv /workdir/webserver/st_files /workdir/webserver/st_files_default \ + && cp /workdir/webserver/active_program /docker_persistent/active_program \ + && mv /workdir/webserver/active_program /workdir/webserver/active_program_default \ + && ln -s /docker_persistent/mbconfig.cfg /workdir/webserver/mbconfig.cfg \ + && ln -s /docker_persistent/persistent.file /workdir/webserver/persistent.file \ + && ln -s /docker_persistent/openplc.db /workdir/webserver/openplc.db \ + && ln -s /docker_persistent/dnp3.cfg /workdir/webserver/dnp3.cfg \ + && ln -s /docker_persistent/st_files /workdir/webserver/st_files \ + && ln -s /docker_persistent/active_program /workdir/webserver/active_program + +ENTRYPOINT ["./start_openplc.sh"] diff --git a/webserver/webserver.py b/webserver/webserver.py index b3defec..cfeb1b7 100644 --- a/webserver/webserver.py +++ b/webserver/webserver.py @@ -17,6 +17,7 @@ import mimetypes import flask import flask_login +from restapi import restapi_bp, register_callback_get, register_callback_post app = flask.Flask(__name__) app.secret_key = str(os.urandom(16)) @@ -25,6 +26,77 @@ login_manager.init_app(app) openplc_runtime = openplc.runtime() +def restapi_callback_get(argument: str, data: dict) -> dict: + """ + This is the central callback function that handles the logic + based on the 'argument' from the URL and 'data' from the request. + """ + # TODO logging debug level + print(f"GET | [{__name__}] Received argument: {argument}, data: {data}") + + if argument == "start-plc": + openplc_runtime.start_runtime() + return {"status": "runtime started"} + + elif argument == "stop-plc": + openplc_runtime.stop_runtime() + return {"status": "runtime stop"} + + elif argument == "runtime-logs": + logs = openplc_runtime.logs() + return {"runtime-logs": logs} + + elif argument == "compilation-status": + status = openplc_runtime.is_compiling + return {"is-compiling": status} + + elif argument == "compilation-logs": + logs = openplc_runtime.compilation_status() + return {"compilation-logs": logs} + + elif argument == "status": + return {"current_status": "operational", "details": data} + + elif argument == "ping": + return {"status": "pong"} + else: + return {"error": "Unknown argument"} + +# file upload POST handler +def restapi_callback_post(argument: str, data: dict) -> dict: + # TODO logging debug level + print(f"POST | [{__name__}] Received argument: {argument}, data: {data}") + + if argument == "upload-file": + try: + # TODO validate filename, content and size + st_file = flask.request.files['file'] + print(st_file.filename) + st_file.save(f"st_files/{st_file.filename}") + return {"UploadFile": "Success"} + + except: + return {"UploadFile": "Fail"} + + elif argument == "compile-program": + if (openplc_runtime.status() == "Compiling"): + return {"RuntimeStatus": "Compiling"} + + try: + # TODO return compilation result and validate filename + # st_file = flask.request.args.get('file') + st_file = flask.request.files['file'] + # print(f"st_files/{st_file.filename}") + openplc_runtime.compile_program(f"{st_file.filename}") + return {"CompilationStatus": "Program Compiled"} + + except Exception as e: + return {"CompilationStatus": e} + + else: + return {"PostError": "Unknown argument"} + + class User(flask_login.UserMixin): pass @@ -852,6 +924,7 @@ def upload_program(): if (prog_file.filename == ''): return draw_blank_page() + "

Error

You need to select a file to be uploaded!

Use the back-arrow on your browser to return

" + # TODO realocate to another function filename = str(random.randint(1,1000000)) + ".st" prog_file.save(os.path.join('st_files', filename)) @@ -2494,6 +2567,11 @@ def main(): print("Starting the web interface...") if __name__ == '__main__': + # rest api register + app.register_blueprint(restapi_bp, url_prefix='/api') + register_callback_get(restapi_callback_get) + register_callback_post(restapi_callback_post) + #Load information about current program on the openplc_runtime object file = open("active_program", "r") st_file = file.read()