From 30a46fd31c475979ab49e11129588634cefc0942 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 10 Oct 2020 12:58:18 +0200 Subject: [PATCH 1/8] Removed socket connection between server and feeder. Now the feeder is a class. Issue #16 --- .env.template | 4 +- NCFeeder/run.py | 24 ------ NCFeeder/socketio_interface.py | 83 ------------------- UIserver/__init__.py | 74 ++++------------- .../hw_controller}/feeder.py | 38 +++++++-- .../hw_controller/feeder_event_manager.py | 29 +++++++ .../hw_controller}/gcode_rescalers.py | 1 - .../queue_manager.py | 2 +- .../socketio_callbacks.py | 56 ++----------- UIserver/sockets_interface/socketio_emits.py | 23 +++++ UIserver/static/js/base.js | 2 +- UIserver/static/js/manual_control.js | 5 +- UIserver/templates/preferences/settings.html | 4 +- UIserver/views/drawings_management.py | 5 +- UIserver/views/settings.py | 2 +- 15 files changed, 118 insertions(+), 234 deletions(-) delete mode 100644 NCFeeder/run.py delete mode 100644 NCFeeder/socketio_interface.py rename {NCFeeder => UIserver/hw_controller}/feeder.py (95%) create mode 100644 UIserver/hw_controller/feeder_event_manager.py rename {NCFeeder => UIserver/hw_controller}/gcode_rescalers.py (99%) rename UIserver/{bot_interface => hw_controller}/queue_manager.py (97%) rename UIserver/{bot_interface => sockets_interface}/socketio_callbacks.py (68%) create mode 100644 UIserver/sockets_interface/socketio_emits.py diff --git a/.env.template b/.env.template index 3418f85f..e850f1dd 100644 --- a/.env.template +++ b/.env.template @@ -3,6 +3,4 @@ # Check the wiki for the available environmental variables FLASK_ENV=development -FLASK_DEBUG=0 -SHOW_FEEDER_TERMINAL=true -RUN_FEEDER_MANUALLY=true \ No newline at end of file +FLASK_DEBUG=0 \ No newline at end of file diff --git a/NCFeeder/run.py b/NCFeeder/run.py deleted file mode 100644 index 78e4c363..00000000 --- a/NCFeeder/run.py +++ /dev/null @@ -1,24 +0,0 @@ -from socketio_interface import SocketInterface -import socketio -from feeder import Feeder -from pid import PidFile -from time import sleep -import traceback -import atexit - -pidname = "feeder.pid" - -try: - with PidFile(pidname) as p: # check if the process is already running using pid files. If it is already running will restart it - - sioif = SocketInterface() - - @atexit.register - def at_exit(): - sioif.at_exit() - # Wait for any event - while True: - pass -except: - print(traceback.print_exc()) - sleep(5) \ No newline at end of file diff --git a/NCFeeder/socketio_interface.py b/NCFeeder/socketio_interface.py deleted file mode 100644 index ebda0254..00000000 --- a/NCFeeder/socketio_interface.py +++ /dev/null @@ -1,83 +0,0 @@ -import socketio -import atexit -from flask_socketio import emit -from feeder import Feeder, FeederEventHandler -import pickle - -sio = socketio.Client() - -def show_toast_on_UI(message): - sio.emit("message_to_frontend", message) - -class FeederEvents(FeederEventHandler): - def on_drawing_ended(self): - # Send a message to the server that the drawing is ended. - print("S> Sending drawing ended") - sio.emit("drawing_ended") - - def on_drawing_started(self): - # Send a message to the server that a drawing has been started. - print("S> Sending drawing started. Code: {}".format(sio.feeder.get_drawing_code())) - sio.emit("drawing_started", sio.feeder.get_drawing_code()) - - def on_message_received(self, line): - # Send the line to the server - sio.emit("message_from_device", line) - - def on_new_line(self, line): - # Send the line to the server - sio.emit("path_command", line) - -class SocketInterface(): - - def __init__(self): - sio.connect('http://127.0.0.1:5000') - print("Socket connection established") - events = FeederEvents() - self.feeder = Feeder(events) - sio.feeder = self.feeder - self.feeder.connect() - - def at_exit(self): - sio.feeder.close() - sio.disconnect() - - def send_command(command): - sio.emit("server_command", command) - - def disconnect(): - sio.disconnect() - - - # Socket events from the server - - # Starts a new drawing (even if there was a drawing on the way already) - @sio.on('bot_start') - def start_gcode(code): - sio.feeder.start_code(code, force_stop = True) - - # Send the current status of the current drawing to the server - @sio.on('bot_status') - def send_status(): - sio.emit("feeder_status", pickle.dumps(sio.feeder.get_status())) - - # Settings callbacks - @sio.on('serial_port_list_request') - def update_serial_port_list(): - print("Sending list of serial ports") - sio.emit("serial_list", pickle.dumps(sio.feeder.serial.serial_port_list())) - - # Connect to device call - @sio.on('connect_to_device') - def connect_to_device(): - sio.feeder.connect() - if sio.feeder.serial.is_connected(): - show_toast_on_UI("Connection to device successful") - else: - show_toast_on_UI("Device not connected. Opening a fake serial port.") - - @sio.on('gcode_command') - def send_gcode_command(command): - print("Received command: " + command) - sio.feeder.send_gcode_command(command) - show_toast_on_UI("Command executed") diff --git a/UIserver/__init__.py b/UIserver/__init__.py index 5538de79..43991b9e 100644 --- a/UIserver/__init__.py +++ b/UIserver/__init__.py @@ -13,17 +13,25 @@ import urllib.request import platform from time import sleep -from UIserver.bot_interface.queue_manager import QueueManager +from UIserver.hw_controller.queue_manager import QueueManager +from UIserver.hw_controller.feeder import Feeder +from UIserver.hw_controller.feeder_event_manager import FeederEventManager +from UIserver.sockets_interface.socketio_emits import SocketioEmits import sass from flask_minify import minify from utils import settings_utils, software_updates app = Flask(__name__, template_folder='templates') + +# Logging setup app.logger.setLevel(logging.INFO) +logging.getLogger("werkzeug").setLevel('WARNING') + app.config['SECRET_KEY'] = 'secret!' # TODO put a key here app.config['UPLOAD_FOLDER'] = "./UIserver/static/Drawings" socketio = SocketIO(app) +app.semits = SocketioEmits(socketio) file_path = os.path.abspath(os.getcwd())+"\database.db" app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+file_path @@ -36,11 +44,16 @@ # js and html minifier (on request) minify(app=app, html=True, js=False) -app.qmanager = QueueManager(app, socketio) import UIserver.database import UIserver.views.drawings_management, UIserver.views.settings -import UIserver.bot_interface.socketio_callbacks +import UIserver.sockets_interface.socketio_callbacks + +# Device controller initialization + +app.feeder = Feeder(FeederEventManager(app)) +app.feeder.connect() +app.qmanager = QueueManager(app, socketio) # Context pre-processor variables # Global template values to be injected before templates creation @@ -70,61 +83,6 @@ def versioned_url_for(endpoint, **values): values["version"] = sw_version return url_for(endpoint, **values) -# This section starts the feeder or restarts it if already running when the server is restarted - -# Wait until the server is ready -def wait_server_ready(): - try: - while urllib.request.urlopen("http://localhost:5000").getcode() != 200: - pass - except Exception as e: - print("__init.py__ error: "+str(e)) - start_feeder_process() - -# run the waiting function in a thread -starter_thread = threading.Thread(target=wait_server_ready, daemon=True) -starter_thread.start() - -# starts the process -def start_feeder_process(): - try: - # If the "RUN_FEEDER_MANUALLY" environment variable is set to 'true', the server will not start the feeder which must then be started manually. - # Can be usefull when working on the feeder and it is not necessary to restart the server every time. - # To start the feeder manually can use "python NCFeeder/run.py" or also the debugger - if os.environ['RUN_FEEDER_MANUALLY'] == 'true': - return - except: - pass - - - # terminal window is available only on windows - if platform.system() == "Windows": - filename = os.path.dirname(__file__) + "\\..\\NCFeeder\\run.py" - - from subprocess import CREATE_NEW_CONSOLE, CREATE_NO_WINDOW - - try: - # Check if the environment variable is set. If it is will show the ncfeeder terminal window, otherwise will keep it hidden - create_window = CREATE_NEW_CONSOLE if os.environ['SHOW_FEEDER_TERMINAL'] == 'true' else CREATE_NO_WINDOW - except: - create_window = CREATE_NO_WINDOW - feeder_process = Popen("env/Scripts/activate.bat & python NCFeeder/run.py", env=os.environ.copy(), creationflags=create_window) - else: - filename = os.path.dirname(__file__) + "/../NCFeeder/run.py" - feeder_process = Popen(["python3", filename], env=os.environ.copy()) - app.feeder_pid = feeder_process.pid - -@atexit.register -def terminate_feeder_process(): - try: - # The feeder_process cannot be killed or terminated if saved into the app directly. - # Instead of saving the process object save the pid and kill it with that - process = psutil.Process(app.feeder_pid) - for proc in process.children(recursive=True): - proc.kill() - process.kill() - except: - pass # Home routes @app.route('/') diff --git a/NCFeeder/feeder.py b/UIserver/hw_controller/feeder.py similarity index 95% rename from NCFeeder/feeder.py rename to UIserver/hw_controller/feeder.py index e8261815..c7f03c1d 100644 --- a/NCFeeder/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -4,7 +4,7 @@ sys.path.insert(1, os.path.join(sys.path[0], '..')) import glob from pathlib import Path -from gcode_rescalers import * +from UIserver.hw_controller.gcode_rescalers import * import time import serial.tools.list_ports import serial @@ -15,13 +15,24 @@ from collections import OrderedDict, deque from copy import deepcopy + +""" + +This class duty is to send commands to the hw. It can be a single command or an entire drawing. + + +""" + +# TODO use different logger + + class FeederEventHandler(): # called when the drawing is finished - def on_drawing_ended(self): + def on_drawing_ended(self, code): pass # called when a new drawing is started - def on_drawing_started(self): + def on_drawing_started(self, code): pass # called when the feeder receives a message from the hw that must be sent to the frontend @@ -51,6 +62,7 @@ def __init__(self, handler = None, **kargvs): else: self.handler = handler self.serial = None self.line_number = 0 + self._timeout_last_line = self.line_number # buffer control attrs self.command_buffer = deque() @@ -90,6 +102,7 @@ def connect(self): def wait_device_ready(self): time.sleep(1) # TODO make it better + # without this function the device may be not ready to receive commands def set_event_handler(self, handler): self.handler = handler @@ -111,7 +124,7 @@ def start_code(self, code, force_stop=False): with self.command_buffer_mutex: self.command_buffer.clear() self._th.start() - self.handler.on_drawing_started() + self.handler.on_drawing_started(code) # ask if the feeder is already sending a file def is_running(self): @@ -153,7 +166,7 @@ def _thf(self, code): print("Starting new drawing with code {}".format(code)) with self.serial_mutex: code = self._running_code - filename = os.path.join(str(Path(__file__).parent.parent.absolute()), "UIserver/static/Drawings/{0}/{0}.gcode".format(code)) + filename = os.path.join(str(Path(__file__).parent.parent.absolute()), "static/Drawings/{0}/{0}.gcode".format(code)) # TODO retrieve saved information for the gcode filter dims = {"table_x":100, "table_y":100, "drawing_max_x":100, "drawing_max_y":100, "drawing_min_x":0, "drawing_min_y":0} @@ -177,7 +190,8 @@ def _thf(self, code): self.send_gcode_command(line) self.send_script(settings['scripts']['after']) - self.handler.on_drawing_ended() + self.handler.on_drawing_ended(code) + self.stop() # thread that keep reading the serial port def _thsr(self): @@ -200,7 +214,7 @@ def _on_timeout(self): print("!Buffer timeout. Try to clean the buffer!") # to clean the buffer try to send an M114 message. In this way will trigger the buffer cleaning mechanism line = self._generate_line("M114") # may need to send it twice? could also send an older line to trigger the error? - with self.serial_mutex: + with self.serial_mutex: self.serial.send(line) else: self._update_timeout() @@ -269,7 +283,7 @@ def parse_device_line(self, line): def get_status(self): with self.serial_mutex: - return {"is_running":self._isrunning, "progress":[self.command_number, self.total_commands_number], "is_paused":self._ispaused, "is_connected":self.serial.is_connected()} + return {"is_running":self._isrunning, "progress":[self.command_number, self.total_commands_number], "is_paused":self._ispaused, "is_connected":self.is_connected()} def _generate_line(self, command): self.line_number += 1 @@ -345,6 +359,12 @@ def reset_line_number(self, line_number = 2): print("Resetting line number") self.send_gcode_command("M110 N{}".format(line_number)) + def serial_ports_list(self): + return self.serial.serial_port_list() + + def is_connected(self): + return self.serial.is_connected() + class DeviceSerial(): def __init__(self, serialname = None, baudrate = None): self.serialname = serialname @@ -363,7 +383,7 @@ def __init__(self, serialname = None, baudrate = None): self.serial.open() print("Serial device connected") except: - print(traceback.print_exc()) + #print(traceback.print_exc()) self.is_fake = True print("Serial not available. Will use the fake serial") diff --git a/UIserver/hw_controller/feeder_event_manager.py b/UIserver/hw_controller/feeder_event_manager.py new file mode 100644 index 00000000..9b31f952 --- /dev/null +++ b/UIserver/hw_controller/feeder_event_manager.py @@ -0,0 +1,29 @@ +from UIserver.hw_controller.feeder import FeederEventHandler + + +class FeederEventManager(FeederEventHandler): + def __init__(self, app): + super().__init__() + self.app = app + + def on_drawing_ended(self, code): + print("S> Sending drawing ended") + self.app.logger.info("B> Drawing ended") + self.app.semits.show_toast_on_UI("Drawing ended") + # nav_drawing_request() # TODO update interface + self.app.qmanager.set_is_drawing(False) + self.app.qmanager.start_next() + + def on_drawing_started(self, code): + self.app.logger.info("B> Drawing started") + self.app.semits.show_toast_on_UI("Drawing started") + self.app.qmanager.set_code(code) + # nav_drawing_request() # TODO update interface + + def on_message_received(self, line): + # Send the line to the server + self.app.semits.hw_command_line_message(line) + + def on_new_line(self, line): + # Send the line to the server + self.app.semits.update_hw_preview(line) \ No newline at end of file diff --git a/NCFeeder/gcode_rescalers.py b/UIserver/hw_controller/gcode_rescalers.py similarity index 99% rename from NCFeeder/gcode_rescalers.py rename to UIserver/hw_controller/gcode_rescalers.py index ccd054ef..2592caa4 100644 --- a/NCFeeder/gcode_rescalers.py +++ b/UIserver/hw_controller/gcode_rescalers.py @@ -1,6 +1,5 @@ import math - # This class is the base class to create different types of stretching/clipping of the drawing to fit it on the table (because the drawing may be for a different table size) # The base class can be extended to get different results # Can rotate the drawings (angle in degrees) diff --git a/UIserver/bot_interface/queue_manager.py b/UIserver/hw_controller/queue_manager.py similarity index 97% rename from UIserver/bot_interface/queue_manager.py rename to UIserver/hw_controller/queue_manager.py index 0ea3a080..a82f4d4f 100644 --- a/UIserver/bot_interface/queue_manager.py +++ b/UIserver/hw_controller/queue_manager.py @@ -76,4 +76,4 @@ def start_next(self, force_stop=False): # This method send a "start" command to the bot with the code of the drawing def start_drawing(self, code): self.app.logger.info("Sending gcode start command") - self.socketio.emit('bot_start', str(code)) + self.app.feeder.start_code(code, force_stop = True) diff --git a/UIserver/bot_interface/socketio_callbacks.py b/UIserver/sockets_interface/socketio_callbacks.py similarity index 68% rename from UIserver/bot_interface/socketio_callbacks.py rename to UIserver/sockets_interface/socketio_callbacks.py index a076e093..7abbd73d 100644 --- a/UIserver/bot_interface/socketio_callbacks.py +++ b/UIserver/sockets_interface/socketio_callbacks.py @@ -5,9 +5,6 @@ import datetime from utils import settings_utils, software_updates -def show_toast_on_UI(message): - socketio.emit("message_toast", message) - @socketio.on('connect') def on_connect(): nav_drawing_request() @@ -83,54 +80,17 @@ def start_playlist(code): @socketio.on("save_settings") def save_settings(data, is_connect): settings_utils.save_settings(data) - show_toast_on_UI("Settings saved") + app.semits.show_toast_on_UI("Settings saved") if is_connect: app.logger.info("Connecting device") - socketio.emit("connect_to_device") + + app.feeder.connect() + if app.feeder.is_connected(): + app.semits.show_toast_on_UI("Connection to device successful") + else: + app.semits.show_toast_on_UI("Device not connected. Opening a fake serial port.") @socketio.on("send_gcode_command") def send_gcode_command(command): - socketio.emit("gcode_command", command) - -# ---- NCFeeder callbacks ---- - -# receives the list of serial ports available and redirect them to the js frontend -@socketio.on('serial_list') -def on_serial_list(slist): - slist = pickle.loads(slist) - app.logger.info(slist) - socketio.emit("serial_list_show", slist) - -@socketio.on('drawing_ended') -def on_drawing_ended(): - app.logger.info("B> Drawing ended") - show_toast_on_UI("Drawing ended") - nav_drawing_request() - app.qmanager.set_is_drawing(False) - app.qmanager.start_next() - -@socketio.on('drawing_started') -def on_drawing_started(code): - app.logger.info("B> Drawing started") - show_toast_on_UI("Drawing started") - app.qmanager.set_code(code) - nav_drawing_request() - -@socketio.on("feeder_status") -def on_feeder_status(status): - feeder = pickle.loads(status) - # TODO show the updated status in the UI - app.logger.info("Status: " + str(feeder)) - -@socketio.on("message_to_frontend") -def message_to_frontend(message): - show_toast_on_UI(message) - -@socketio.on("message_from_device") -def message_from_device(message): - socketio.emit("frontend_message_from_device", message) - -@socketio.on("path_command") -def path_command(line): - socketio.emit("frontend_path_command", line) \ No newline at end of file + app.feeder.send_gcode_command(command) diff --git a/UIserver/sockets_interface/socketio_emits.py b/UIserver/sockets_interface/socketio_emits.py new file mode 100644 index 00000000..9eb20c1c --- /dev/null +++ b/UIserver/sockets_interface/socketio_emits.py @@ -0,0 +1,23 @@ + +class SocketioEmits(): + def __init__(self, socketio): + self.socketio = socketio + + # shows a toast on the interface + def show_toast_on_UI(self, message): + self.socketio.emit("toast_show_message", message) + + + # shows a line coming from the hw device on the manual control panel + def hw_command_line_message(self, line): + self.socketio.emit("command_line_show", line) + + + # sends the last position to update the preview box + def update_hw_preview(self, line): + self.socketio.emit("preview_new_position", line) + + + # general emit + def emit(self, topic, line): + self.socketio.emit(topic, line) \ No newline at end of file diff --git a/UIserver/static/js/base.js b/UIserver/static/js/base.js index c732adfe..8aecd19f 100644 --- a/UIserver/static/js/base.js +++ b/UIserver/static/js/base.js @@ -7,7 +7,7 @@ function document_ready(){}; $( document ).ready(function() { // socket callbacks setup - socket.on('message_toast', function(message){ + socket.on('toast_show_message', function(message){ show_toast(message); }); diff --git a/UIserver/static/js/manual_control.js b/UIserver/static/js/manual_control.js index 552dac9e..ab2137e5 100644 --- a/UIserver/static/js/manual_control.js +++ b/UIserver/static/js/manual_control.js @@ -28,7 +28,7 @@ function prepare_command_window(){ } }); - socket.on("frontend_message_from_device", function(data){ + socket.on("command_line_show", function(data){ add_command_line(data); }); } @@ -102,8 +102,9 @@ function prepare_canvas(){ clear_canvas(); - socket.on('frontend_path_command', function(line){ + socket.on("preview_new_position", function(line){ console.log("Received line: " + line); + add_command_line(line); if(line.includes("G28")){ clear_canvas(); // TODO add some sort of animation/fading diff --git a/UIserver/templates/preferences/settings.html b/UIserver/templates/preferences/settings.html index 6757f7f8..ab0c0610 100644 --- a/UIserver/templates/preferences/settings.html +++ b/UIserver/templates/preferences/settings.html @@ -15,7 +15,9 @@

Serial port settings

Port number
diff --git a/UIserver/views/drawings_management.py b/UIserver/views/drawings_management.py index b9d36583..a3aeccdb 100644 --- a/UIserver/views/drawings_management.py +++ b/UIserver/views/drawings_management.py @@ -3,7 +3,7 @@ from flask import render_template, request, url_for, redirect from werkzeug.utils import secure_filename from utils.gcode_converter import gcode_to_image -from UIserver.bot_interface.socketio_callbacks import add_to_playlist +from UIserver.sockets_interface.socketio_callbacks import add_to_playlist import traceback import datetime @@ -153,7 +153,8 @@ def delete_playlist(code): # Show queue @app.route('/queue') def show_queue(): - socketio.emit("bot_status") + status = app.feeder.get_status() # TODO pass status to the template + print(status) code = app.qmanager.get_code() if not code is None: item = db.session.query(UploadedFiles).filter_by(id=code).first() diff --git a/UIserver/views/settings.py b/UIserver/views/settings.py index 4cf6bb07..ecd7f31a 100644 --- a/UIserver/views/settings.py +++ b/UIserver/views/settings.py @@ -22,7 +22,7 @@ def settings_page(): serial["baudrates"] = ["2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"] serial["baud"] = settings['serial']['baud'] # TODO load the last saved serial["port"] = settings['serial']['port'] # TODO load the last saved - socketio.emit("serial_port_list_request") + serial["available_ports"] = app.feeder.serial_port_list() return render_template("preferences/settings.html", serial = serial, settings = settings) # Reboot the device From 32b61bf6e32fef185db6f6626f3c238fc97243c1 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 10 Oct 2020 13:12:58 +0200 Subject: [PATCH 2/8] Fixed serial port selection --- UIserver/hw_controller/feeder.py | 3 ++- UIserver/static/js/settings.js | 13 ------------- UIserver/templates/preferences/settings.html | 2 +- UIserver/views/settings.py | 7 ++++--- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/UIserver/hw_controller/feeder.py b/UIserver/hw_controller/feeder.py index c7f03c1d..bd7ad6a1 100644 --- a/UIserver/hw_controller/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -360,7 +360,8 @@ def reset_line_number(self, line_number = 2): self.send_gcode_command("M110 N{}".format(line_number)) def serial_ports_list(self): - return self.serial.serial_port_list() + result = self.serial.serial_port_list() + return [] if result is None else result def is_connected(self): return self.serial.is_connected() diff --git a/UIserver/static/js/settings.js b/UIserver/static/js/settings.js index 147dd778..03251f5d 100644 --- a/UIserver/static/js/settings.js +++ b/UIserver/static/js/settings.js @@ -1,17 +1,4 @@ function document_ready(){ - socket.on("serial_list_show", function(data){ - console.log("list_request"); - console.log(data); - var options = []; - data.push("FAKE") - var selector = $("#serial_ports") - selector.html(" ") - var selected_value = $("#saved_port").html() - for (var i = 0; i < data.length; i++){ - selector.append($("").attr("value", data[i]).text(data[i])); - } - selector.val(selected_value); - }); }; function save(connect = false){ diff --git a/UIserver/templates/preferences/settings.html b/UIserver/templates/preferences/settings.html index ab0c0610..c364fa19 100644 --- a/UIserver/templates/preferences/settings.html +++ b/UIserver/templates/preferences/settings.html @@ -16,7 +16,7 @@

Serial port settings

Port number
diff --git a/UIserver/views/settings.py b/UIserver/views/settings.py index ecd7f31a..656cbbf1 100644 --- a/UIserver/views/settings.py +++ b/UIserver/views/settings.py @@ -20,9 +20,10 @@ def settings_page(): settings = settings_utils.load_settings() serial = {} serial["baudrates"] = ["2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"] - serial["baud"] = settings['serial']['baud'] # TODO load the last saved - serial["port"] = settings['serial']['port'] # TODO load the last saved - serial["available_ports"] = app.feeder.serial_port_list() + serial["baud"] = settings['serial']['baud'] # load the last saved + serial["port"] = settings['serial']['port'] # load the last saved + serial["available_ports"] = app.feeder.serial_ports_list() + serial["available_ports"].append("FAKE") return render_template("preferences/settings.html", serial = serial, settings = settings) # Reboot the device From dc8ed6caf0ceca74630485e2a7b2e4d8a9544930 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 10 Oct 2020 13:51:18 +0200 Subject: [PATCH 3/8] Fixed status preview in navbar --- UIserver/__init__.py | 6 +++-- .../hw_controller/feeder_event_manager.py | 5 ++-- .../sockets_interface/socketio_callbacks.py | 18 +------------ UIserver/sockets_interface/socketio_emits.py | 27 +++++++++++++++---- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/UIserver/__init__.py b/UIserver/__init__.py index 43991b9e..b0996295 100644 --- a/UIserver/__init__.py +++ b/UIserver/__init__.py @@ -16,7 +16,6 @@ from UIserver.hw_controller.queue_manager import QueueManager from UIserver.hw_controller.feeder import Feeder from UIserver.hw_controller.feeder_event_manager import FeederEventManager -from UIserver.sockets_interface.socketio_emits import SocketioEmits import sass from flask_minify import minify from utils import settings_utils, software_updates @@ -31,7 +30,6 @@ app.config['SECRET_KEY'] = 'secret!' # TODO put a key here app.config['UPLOAD_FOLDER'] = "./UIserver/static/Drawings" socketio = SocketIO(app) -app.semits = SocketioEmits(socketio) file_path = os.path.abspath(os.getcwd())+"\database.db" app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+file_path @@ -39,6 +37,7 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) + # scss compiler (already minified) sass.compile(dirname=(os.path.abspath(os.getcwd())+"/UIserver/static/scss", os.path.abspath(os.getcwd())+"/UIserver/static/css"), output_style='compressed') # js and html minifier (on request) @@ -48,6 +47,9 @@ import UIserver.database import UIserver.views.drawings_management, UIserver.views.settings import UIserver.sockets_interface.socketio_callbacks +from UIserver.sockets_interface.socketio_emits import SocketioEmits + +app.semits = SocketioEmits(app,socketio, db) # Device controller initialization diff --git a/UIserver/hw_controller/feeder_event_manager.py b/UIserver/hw_controller/feeder_event_manager.py index 9b31f952..02dd1f13 100644 --- a/UIserver/hw_controller/feeder_event_manager.py +++ b/UIserver/hw_controller/feeder_event_manager.py @@ -1,6 +1,5 @@ from UIserver.hw_controller.feeder import FeederEventHandler - class FeederEventManager(FeederEventHandler): def __init__(self, app): super().__init__() @@ -10,7 +9,7 @@ def on_drawing_ended(self, code): print("S> Sending drawing ended") self.app.logger.info("B> Drawing ended") self.app.semits.show_toast_on_UI("Drawing ended") - # nav_drawing_request() # TODO update interface + self.app.semits.send_nav_drawing_status() self.app.qmanager.set_is_drawing(False) self.app.qmanager.start_next() @@ -18,7 +17,7 @@ def on_drawing_started(self, code): self.app.logger.info("B> Drawing started") self.app.semits.show_toast_on_UI("Drawing started") self.app.qmanager.set_code(code) - # nav_drawing_request() # TODO update interface + self.app.semits.send_nav_drawing_status() def on_message_received(self, line): # Send the line to the server diff --git a/UIserver/sockets_interface/socketio_callbacks.py b/UIserver/sockets_interface/socketio_callbacks.py index 7abbd73d..f6fbd5ea 100644 --- a/UIserver/sockets_interface/socketio_callbacks.py +++ b/UIserver/sockets_interface/socketio_callbacks.py @@ -5,14 +5,6 @@ import datetime from utils import settings_utils, software_updates -@socketio.on('connect') -def on_connect(): - nav_drawing_request() - #app.logger.info("Connected") - pass - - -# ---- Frontend callbacks ---- @socketio.on('message') def handle_message(message): @@ -37,15 +29,7 @@ def handle_software_updates_check(): @socketio.on("request_nav_drawing_status") def nav_drawing_request(): - if app.qmanager.is_drawing(): - try: - item = db.session.query(UploadedFiles).filter(UploadedFiles.id==app.qmanager.get_code()).one() - socketio.emit("current_drawing_preview", render_template("drawing_status.html", item=item)) - except: - app.logger.error("Error during nav drawing status update") - socketio.emit("current_drawing_preview", "") - else: - socketio.emit("current_drawing_preview", "") + app.semits.send_nav_drawing_status() # playlist sockets # save the changes to the playlist diff --git a/UIserver/sockets_interface/socketio_emits.py b/UIserver/sockets_interface/socketio_emits.py index 9eb20c1c..54bccc93 100644 --- a/UIserver/sockets_interface/socketio_emits.py +++ b/UIserver/sockets_interface/socketio_emits.py @@ -1,22 +1,39 @@ +import traceback +from UIserver.database import UploadedFiles +from flask import render_template class SocketioEmits(): - def __init__(self, socketio): + def __init__(self, app, socketio, db): + self.app = app self.socketio = socketio + self.db = db # shows a toast on the interface def show_toast_on_UI(self, message): - self.socketio.emit("toast_show_message", message) + self.emit("toast_show_message", message) # shows a line coming from the hw device on the manual control panel def hw_command_line_message(self, line): - self.socketio.emit("command_line_show", line) + self.emit("command_line_show", line) # sends the last position to update the preview box def update_hw_preview(self, line): - self.socketio.emit("preview_new_position", line) - + self.emit("preview_new_position", line) + + + # updates the nav bar status preview + def send_nav_drawing_status(self): + if self.app.qmanager.is_drawing(): + try: + item = self.db.session.query(UploadedFiles).filter(UploadedFiles.id == self.app.qmanager.get_code()).one() + self.emit("current_drawing_preview", render_template("drawing_status.html", item=item)) + except Exception as e: + self.app.logger.error("Error during nav drawing status update") + self.emit("current_drawing_preview", "") + else: + self.emit("current_drawing_preview", "") # general emit def emit(self, topic, line): From f3a208babe0e51bad1af6e5695fd8aa2e8c69e5a Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 10 Oct 2020 14:16:13 +0200 Subject: [PATCH 4/8] Rearranged utils and feeder file --- UIserver/__init__.py | 2 +- UIserver/hw_controller/device_serial.py | 81 ++++++++++ UIserver/hw_controller/feeder.py | 146 ++---------------- .../sockets_interface/socketio_callbacks.py | 2 +- UIserver/utils/buffered_timeout.py | 35 +++++ {utils => UIserver/utils}/gcode_converter.py | 0 UIserver/utils/limited_size_dict.py | 19 +++ {utils => UIserver/utils}/settings_utils.py | 0 {utils => UIserver/utils}/software_updates.py | 0 UIserver/views/drawings_management.py | 2 +- UIserver/views/settings.py | 2 +- setup.py | 2 +- utils/.gitignore | 1 - 13 files changed, 151 insertions(+), 141 deletions(-) create mode 100644 UIserver/hw_controller/device_serial.py create mode 100644 UIserver/utils/buffered_timeout.py rename {utils => UIserver/utils}/gcode_converter.py (100%) create mode 100644 UIserver/utils/limited_size_dict.py rename {utils => UIserver/utils}/settings_utils.py (100%) rename {utils => UIserver/utils}/software_updates.py (100%) delete mode 100644 utils/.gitignore diff --git a/UIserver/__init__.py b/UIserver/__init__.py index b0996295..e50badb3 100644 --- a/UIserver/__init__.py +++ b/UIserver/__init__.py @@ -18,7 +18,7 @@ from UIserver.hw_controller.feeder_event_manager import FeederEventManager import sass from flask_minify import minify -from utils import settings_utils, software_updates +from UIserver.utils import settings_utils, software_updates app = Flask(__name__, template_folder='templates') diff --git a/UIserver/hw_controller/device_serial.py b/UIserver/hw_controller/device_serial.py new file mode 100644 index 00000000..5bce56c7 --- /dev/null +++ b/UIserver/hw_controller/device_serial.py @@ -0,0 +1,81 @@ +import serial.tools.list_ports +import serial +import time +import sys + +# This class connect to a serial device +# If the serial device request is not available it will create a virtual serial device + + +class DeviceSerial(): + def __init__(self, serialname = None, baudrate = None): + self.serialname = serialname + self.baudrate = baudrate + self.is_fake = False + self._buffer = bytearray() + self.echo = "" + try: + args = dict( + baudrate = self.baudrate, + timeout = 0, + write_timeout = 0 + ) + self.serial = serial.Serial(**args) + self.serial.port = self.serialname + self.serial.open() + print("Serial device connected") + except: + #print(traceback.print_exc()) + self.is_fake = True + print("Serial not available. Will use the fake serial") + + def send(self, obj): + if self.is_fake: + print("Fake> " + str(obj)) + self.echo = obj + time.sleep(0.05) + else: + if self.serial.is_open: + try: + while self.readline(): + pass + self.serial.write(str(obj).encode()) + except: + self.close() + print("Error while sending a command") + + def serial_port_list(self): + if sys.platform.startswith('win'): + plist = serial.tools.list_ports.comports() + ports = [port.device for port in plist] + elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): + # this excludes your current terminal "/dev/tty" + ports = glob.glob('/dev/tty[A-Za-z]*') + else: + raise EnvironmentError('Unsupported platform') + return ports + + def is_connected(self): + if(self.is_fake): + return False + return self.serial.is_open + + def close(self): + try: + self.serial.close() + print("Serial port closed") + except: + print("Error: serial already closed or not available") + + def readline(self): + if not self.is_fake: + if self.serial.is_open: + while self.serial.inWaiting(): + line = self.serial.readline() + return line.decode(encoding='UTF-8') + else: + if not self.echo == "": + echo = "ok" # sends "ok" as ack otherwise the feeder will stop sending buffered commands + self.echo = "" + return echo + return None diff --git a/UIserver/hw_controller/feeder.py b/UIserver/hw_controller/feeder.py index bd7ad6a1..fec920cb 100644 --- a/UIserver/hw_controller/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -1,19 +1,15 @@ from threading import Thread, Lock import os -import sys -sys.path.insert(1, os.path.join(sys.path[0], '..')) -import glob from pathlib import Path -from UIserver.hw_controller.gcode_rescalers import * import time -import serial.tools.list_ports -import serial -import atexit import traceback import json -from utils import settings_utils -from collections import OrderedDict, deque +from UIserver.utils import settings_utils +from collections import deque from copy import deepcopy +from UIserver.utils import limited_size_dict, buffered_timeout +from UIserver.hw_controller.device_serial import DeviceSerial +from UIserver.hw_controller.gcode_rescalers import * """ @@ -66,12 +62,12 @@ def __init__(self, handler = None, **kargvs): # buffer control attrs self.command_buffer = deque() - self.command_buffer_mutex = Lock() # mutex used to modify the command buffer - self.command_send_mutex = Lock() # mutex used to pause the thread when the buffer is full + self.command_buffer_mutex = Lock() # mutex used to modify the command buffer + self.command_send_mutex = Lock() # mutex used to pause the thread when the buffer is full self.command_buffer_max_length = 8 - self.command_buffer_history = LimitedSizeDict(size_limit = self.command_buffer_max_length+10) # keep saved the last n commands - self.position_request_difference = 10 # every n lines requires the current position with M114 - self._timeout = BufferTimeout(20, self._on_timeout) + self.command_buffer_history = limited_size_dict.LimitedSizeDict(size_limit = self.command_buffer_max_length+10) # keep saved the last n commands + self.position_request_difference = 10 # every n lines requires the current position with M114 + self._timeout = buffered_timeout.BufferTimeout(20, self._on_timeout) self._timeout.start() def close(self): @@ -190,8 +186,8 @@ def _thf(self, code): self.send_gcode_command(line) self.send_script(settings['scripts']['after']) - self.handler.on_drawing_ended(code) self.stop() + self.handler.on_drawing_ended(code) # thread that keep reading the serial port def _thsr(self): @@ -365,123 +361,3 @@ def serial_ports_list(self): def is_connected(self): return self.serial.is_connected() - -class DeviceSerial(): - def __init__(self, serialname = None, baudrate = None): - self.serialname = serialname - self.baudrate = baudrate - self.is_fake = False - self._buffer = bytearray() - self.echo = "" - try: - args = dict( - baudrate = self.baudrate, - timeout = 0, - write_timeout = 0 - ) - self.serial = serial.Serial(**args) - self.serial.port = self.serialname - self.serial.open() - print("Serial device connected") - except: - #print(traceback.print_exc()) - self.is_fake = True - print("Serial not available. Will use the fake serial") - - def send(self, obj): - if self.is_fake: - print("Fake> " + str(obj)) - self.echo = obj - time.sleep(0.05) - else: - if self.serial.is_open: - try: - while self.readline(): - pass - self.serial.write(str(obj).encode()) - except: - self.close() - print("Error while sending a command") - - def serial_port_list(self): - if sys.platform.startswith('win'): - plist = serial.tools.list_ports.comports() - ports = [port.device for port in plist] - elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): - # this excludes your current terminal "/dev/tty" - ports = glob.glob('/dev/tty[A-Za-z]*') - else: - raise EnvironmentError('Unsupported platform') - return ports - - def is_connected(self): - if(self.is_fake): - return False - return self.serial.is_open - - def close(self): - try: - self.serial.close() - print("Serial port closed") - except: - print("Error: serial already closed or not available") - - def readline(self): - if not self.is_fake: - if self.serial.is_open: - while self.serial.inWaiting(): - line = self.serial.readline() - return line.decode(encoding='UTF-8') - else: - if not self.echo == "": - echo = "ok" # sends "ok" as ack otherwise the feeder will stop sending buffered commands - self.echo = "" - return echo - return None - -class LimitedSizeDict(OrderedDict): - def __init__(self, *args, **kwds): - self.size_limit = kwds.pop("size_limit", None) - OrderedDict.__init__(self, *args, **kwds) - self._check_size_limit() - - def __setitem__(self, key, value): - OrderedDict.__setitem__(self, key, value) - self._check_size_limit() - - def _check_size_limit(self): - if self.size_limit is not None: - while len(self) > self.size_limit: - self.popitem(last=False) - -# this thread calls a function after a timeout but only if the "update" method is not called before that timeout expires -class BufferTimeout(Thread): - def __init__(self, timeout_delta, function, group=None, target=None, name=None, args=(), kwargs=None): - super(BufferTimeout, self).__init__(group=group, target=target, name=name) - self.timeout_delta = timeout_delta - self.callback = function - self.mutex = Lock() - self.is_running = False - self.setDaemon(True) - self.update() - - def update(self): - with self.mutex: - self.timeout_time = time.time() + self.timeout_delta - - def stop(self): - with self.mutex: - self.is_running = False - - def run(self): - self.is_running = True - while self.is_running: - with self.mutex: - timeout = self.timeout_time - current_time = time.time() - if current_time > timeout: - self.callback() - self.update() - with self.mutex: - timeout = self.timeout_time - time.sleep(timeout - current_time) diff --git a/UIserver/sockets_interface/socketio_callbacks.py b/UIserver/sockets_interface/socketio_callbacks.py index f6fbd5ea..7bc3ae0f 100644 --- a/UIserver/sockets_interface/socketio_callbacks.py +++ b/UIserver/sockets_interface/socketio_callbacks.py @@ -3,7 +3,7 @@ from UIserver.database import UploadedFiles, Playlists import pickle import datetime -from utils import settings_utils, software_updates +from UIserver.utils import settings_utils, software_updates @socketio.on('message') diff --git a/UIserver/utils/buffered_timeout.py b/UIserver/utils/buffered_timeout.py new file mode 100644 index 00000000..a8a31365 --- /dev/null +++ b/UIserver/utils/buffered_timeout.py @@ -0,0 +1,35 @@ +from threading import Thread, Lock +import time + +# this thread calls a function after a timeout but only if the "update" method is not called before that timeout expires + +class BufferTimeout(Thread): + def __init__(self, timeout_delta, function, group=None, target=None, name=None, args=(), kwargs=None): + super(BufferTimeout, self).__init__(group=group, target=target, name=name) + self.timeout_delta = timeout_delta + self.callback = function + self.mutex = Lock() + self.is_running = False + self.setDaemon(True) + self.update() + + def update(self): + with self.mutex: + self.timeout_time = time.time() + self.timeout_delta + + def stop(self): + with self.mutex: + self.is_running = False + + def run(self): + self.is_running = True + while self.is_running: + with self.mutex: + timeout = self.timeout_time + current_time = time.time() + if current_time > timeout: + self.callback() + self.update() + with self.mutex: + timeout = self.timeout_time + time.sleep(timeout - current_time) diff --git a/utils/gcode_converter.py b/UIserver/utils/gcode_converter.py similarity index 100% rename from utils/gcode_converter.py rename to UIserver/utils/gcode_converter.py diff --git a/UIserver/utils/limited_size_dict.py b/UIserver/utils/limited_size_dict.py new file mode 100644 index 00000000..4d4679e3 --- /dev/null +++ b/UIserver/utils/limited_size_dict.py @@ -0,0 +1,19 @@ +from collections import OrderedDict + +# This dict class can have a size limit +# Every time a new item is added to the dict, the oldest will be removed + +class LimitedSizeDict(OrderedDict): + def __init__(self, *args, **kwds): + self.size_limit = kwds.pop("size_limit", None) + OrderedDict.__init__(self, *args, **kwds) + self._check_size_limit() + + def __setitem__(self, key, value): + OrderedDict.__setitem__(self, key, value) + self._check_size_limit() + + def _check_size_limit(self): + if self.size_limit is not None: + while len(self) > self.size_limit: + self.popitem(last=False) diff --git a/utils/settings_utils.py b/UIserver/utils/settings_utils.py similarity index 100% rename from utils/settings_utils.py rename to UIserver/utils/settings_utils.py diff --git a/utils/software_updates.py b/UIserver/utils/software_updates.py similarity index 100% rename from utils/software_updates.py rename to UIserver/utils/software_updates.py diff --git a/UIserver/views/drawings_management.py b/UIserver/views/drawings_management.py index a3aeccdb..067fb6ee 100644 --- a/UIserver/views/drawings_management.py +++ b/UIserver/views/drawings_management.py @@ -2,7 +2,7 @@ from UIserver.database import UploadedFiles, Playlists from flask import render_template, request, url_for, redirect from werkzeug.utils import secure_filename -from utils.gcode_converter import gcode_to_image +from UIserver.utils.gcode_converter import gcode_to_image from UIserver.sockets_interface.socketio_callbacks import add_to_playlist import traceback import datetime diff --git a/UIserver/views/settings.py b/UIserver/views/settings.py index 656cbbf1..4649cf08 100644 --- a/UIserver/views/settings.py +++ b/UIserver/views/settings.py @@ -3,7 +3,7 @@ import os.path import shutil import json -from utils import settings_utils +from UIserver.utils import settings_utils import os from time import sleep from threading import Thread diff --git a/setup.py b/setup.py index 4811c0e6..4c494c03 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import time import platform import os -from utils import settings_utils +from UIserver.utils import settings_utils class PostDevelopCommand(develop): def run(self): diff --git a/utils/.gitignore b/utils/.gitignore deleted file mode 100644 index a28d5780..00000000 --- a/utils/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test.gcode \ No newline at end of file From 4cf8d883829a32f6af294fde4a0a34eca06570f3 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 10 Oct 2020 15:47:52 +0200 Subject: [PATCH 5/8] Adding loggin with custom levels with control over .env file --- .env.template | 8 +- UIserver/__init__.py | 30 ++++++-- UIserver/hw_controller/device_serial.py | 16 ++-- UIserver/hw_controller/feeder.py | 73 +++++++++++-------- .../hw_controller/feeder_event_manager.py | 1 - UIserver/utils/settings_utils.py | 31 +++++++- UIserver/views/drawings_management.py | 2 - 7 files changed, 112 insertions(+), 49 deletions(-) diff --git a/.env.template b/.env.template index e850f1dd..468d018c 100644 --- a/.env.template +++ b/.env.template @@ -3,4 +3,10 @@ # Check the wiki for the available environmental variables FLASK_ENV=development -FLASK_DEBUG=0 \ No newline at end of file +FLASK_DEBUG=0 + +# feeder logger level: 5 -> acks received from the device (and above), level 6 -> lines sent to the device (and above), other standard logging levels of python +FEEDER_LEVEL=5 + +# flask logger level: uses standard python loggin levels (10-debug, 20-info, 30-warning, 40-error, 50-critical). Can set to warning to hide standard http requests +FLASK_LEVEL=30 \ No newline at end of file diff --git a/UIserver/__init__.py b/UIserver/__init__.py index e50badb3..6e41021f 100644 --- a/UIserver/__init__.py +++ b/UIserver/__init__.py @@ -2,30 +2,44 @@ from flask_socketio import SocketIO from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate + import os import sys -import logging +import platform + from subprocess import Popen import psutil import threading import atexit import signal import urllib.request -import platform + from time import sleep +from dotenv import load_dotenv +import logging + +import sass +from flask_minify import minify + from UIserver.hw_controller.queue_manager import QueueManager from UIserver.hw_controller.feeder import Feeder from UIserver.hw_controller.feeder_event_manager import FeederEventManager -import sass -from flask_minify import minify from UIserver.utils import settings_utils, software_updates -app = Flask(__name__, template_folder='templates') -# Logging setup -app.logger.setLevel(logging.INFO) -logging.getLogger("werkzeug").setLevel('WARNING') +# Logging setup +load_dotenv() +level = os.getenv("FLASK_LEVEL") +if not level is None: + level = int(level) +else: + level = 0 +settings_utils.print_level(level, "app") +logging.getLogger("werkzeug").setLevel(level) + +# app setup +app = Flask(__name__, template_folder='templates') app.config['SECRET_KEY'] = 'secret!' # TODO put a key here app.config['UPLOAD_FOLDER'] = "./UIserver/static/Drawings" diff --git a/UIserver/hw_controller/device_serial.py b/UIserver/hw_controller/device_serial.py index 5bce56c7..271827a2 100644 --- a/UIserver/hw_controller/device_serial.py +++ b/UIserver/hw_controller/device_serial.py @@ -2,13 +2,15 @@ import serial import time import sys +import logging # This class connect to a serial device # If the serial device request is not available it will create a virtual serial device class DeviceSerial(): - def __init__(self, serialname = None, baudrate = None): + def __init__(self, serialname = None, baudrate = None, logger_name = None): + self.logger = logging.getLogger(logger_name) if not logger_name is None else logging.getLogger() self.serialname = serialname self.baudrate = baudrate self.is_fake = False @@ -23,15 +25,15 @@ def __init__(self, serialname = None, baudrate = None): self.serial = serial.Serial(**args) self.serial.port = self.serialname self.serial.open() - print("Serial device connected") + self.logger.info("Serial device connected") except: #print(traceback.print_exc()) self.is_fake = True - print("Serial not available. Will use the fake serial") + self.logger.error("Serial not available. Will use the fake serial") def send(self, obj): if self.is_fake: - print("Fake> " + str(obj)) + #print("Fake> " + str(obj)) self.echo = obj time.sleep(0.05) else: @@ -42,7 +44,7 @@ def send(self, obj): self.serial.write(str(obj).encode()) except: self.close() - print("Error while sending a command") + self.logger.error("Error while sending a command") def serial_port_list(self): if sys.platform.startswith('win'): @@ -63,9 +65,9 @@ def is_connected(self): def close(self): try: self.serial.close() - print("Serial port closed") + self.logger.info("Serial port closed") except: - print("Error: serial already closed or not available") + self.logger.error("Error: serial already closed or not available") def readline(self): if not self.is_fake: diff --git a/UIserver/hw_controller/feeder.py b/UIserver/hw_controller/feeder.py index fec920cb..65c3f6f4 100644 --- a/UIserver/hw_controller/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -4,14 +4,18 @@ import time import traceback import json -from UIserver.utils import settings_utils from collections import deque from copy import deepcopy -from UIserver.utils import limited_size_dict, buffered_timeout + +import logging +from dotenv import load_dotenv + +from UIserver.utils import limited_size_dict, buffered_timeout, settings_utils from UIserver.hw_controller.device_serial import DeviceSerial from UIserver.hw_controller.gcode_rescalers import * + """ This class duty is to send commands to the hw. It can be a single command or an entire drawing. @@ -39,13 +43,29 @@ def on_message_received(self, line): def on_new_line(self, line): pass + + # List of commands that are buffered by the controller BUFFERED_COMMANDS = ("G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03", "G28") + class Feeder(): def __init__(self, handler = None, **kargvs): - self._print_ack = kargvs.pop("print_ack", False) - self._print_ack = True + # logger setup + self.logger = logging.getLogger(__name__) + logging.addLevelName(settings_utils.LINE_SENT, "LINE_SENT") + logging.addLevelName(settings_utils.LINE_RECEIVED, "LINE_RECEIVED") + # load logging level from environment variables + load_dotenv() + level = os.getenv("FEEDER_LEVEL") + if not level is None: + level = int(level) + else: + level = 0 + self.logger.setLevel(level) + + settings_utils.print_level(level, __name__.split(".")[-1]) + self._isrunning = False self._ispaused = False self.total_commands_number = None @@ -74,19 +94,19 @@ def close(self): self.serial.close() def connect(self): - print("Connecting to serial device...") + self.logger.info("Connecting to serial device...") settings = settings_utils.load_settings() with self.serial_mutex: if not self.serial is None: self.serial.close() try: - self.serial = DeviceSerial(settings['serial']['port'], settings['serial']['baud']) + self.serial = DeviceSerial(settings['serial']['port'], settings['serial']['baud'], logger_name = __name__) self._serial_read_thread = Thread(target = self._thsr, daemon=True) self._serial_read_thread.start() except: - print("Error during device connection") - print(traceback.print_exc()) - self.serial = DeviceSerial() + self.logger.info("Error during device connection") + self.logger.info(traceback.print_exc()) + self.serial = DeviceSerial(logger_name = __name__) # wait for the device to be ready self.wait_device_ready() @@ -159,7 +179,7 @@ def _thf(self, code): settings = settings_utils.load_settings() self.send_script(settings['scripts']['before']) - print("Starting new drawing with code {}".format(code)) + self.logger.info("Starting new drawing with code {}".format(code)) with self.serial_mutex: code = self._running_code filename = os.path.join(str(Path(__file__).parent.parent.absolute()), "static/Drawings/{0}/{0}.gcode".format(code)) @@ -196,8 +216,8 @@ def _thsr(self): try: line = self.serial.readline() except Exception as e: - print(e) - print("Serial connection lost") + self.logger.error(e) + self.logger.error("Serial connection lost") if not line is None: self.parse_device_line(line) @@ -207,7 +227,7 @@ def _update_timeout(self): def _on_timeout(self): if (self.command_buffer_mutex.locked and self.line_number == self._timeout_last_line): - print("!Buffer timeout. Try to clean the buffer!") + # self.logger.warning("!Buffer timeout. Trying to clean the buffer!") # to clean the buffer try to send an M114 message. In this way will trigger the buffer cleaning mechanism line = self._generate_line("M114") # may need to send it twice? could also send an older line to trigger the error? with self.serial_mutex: @@ -237,15 +257,12 @@ def _ack_received(self, safe_line_number=None, append_left_extra=False): # parse a line coming from the device def parse_device_line(self, line): - print_line = True if ("start" in line): self.wait_device_ready() self.reset_line_number() elif "ok" in line: # when an "ack" is received free one place in the buffer self._ack_received() - if not self._print_ack: - print_line = False elif "Resend: " in line: line_found = False @@ -266,15 +283,13 @@ def parse_device_line(self, line): self._ack_received(safe_line_number=line_number-1, append_left_extra=True) # the resend command is sending an ack. should add an entry to the buffer to keep the right lenght (because the line has been sent 2 times) if not line_found: - print("No line was found for the number required. Restart numeration.") + self.logger.error("No line was found for the number required. Restart numeration.") self.send_gcode_command("M110 N1") - print_line = False elif "echo:Unknown command:" in line: - print("Error: command not found. Can also be a communication error") + self.logger.error("Error: command not found. Can also be a communication error") - if print_line: - print(line) + self.logger.log(settings_utils.LINE_RECEIVED, line) self.handler.on_message_received(line) def get_status(self): @@ -317,7 +332,7 @@ def send_gcode_command(self, command): # check if the command is in the "BUFFERED_COMMANDS" list and stops if the buffer is full if any(code in command for code in BUFFERED_COMMANDS): - with self.command_send_mutex: # wait until get some "ok" command to remove an element from the buffer + with self.command_send_mutex: # wait until get some "ok" command to remove an element from the buffer pass # send the command after parsing the content @@ -325,34 +340,34 @@ def send_gcode_command(self, command): with self.serial_mutex: # check if needs to send a "M114" command (actual position request) but not in the first lines if (self.line_number % self.position_request_difference) == 0 and self.line_number > 5: - #self._generate_line("M114") # does not send the line to trigger the "resend" event and clean the buffer from messages that are already done + #self._generate_line("M114") # does not send the line to trigger the "resend" event and clean the buffer from messages that are already done pass line = self._generate_line(command) - self.serial.send(line) # send line + self.serial.send(line) # send line + self.logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) # TODO the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident - self._update_timeout() # update the timeout because a new command has been sent + self._update_timeout() # update the timeout because a new command has been sent with self.command_buffer_mutex: if(len(self.command_buffer)>=self.command_buffer_max_length): self.command_send_mutex.acquire() # if the buffer is full acquire the lock so that cannot send new lines until the reception of an ack. Notice that this will stop only buffered commands. The other commands will be sent anyway - self.handler.on_new_line(line) # uses the handler callback for the new line + self.handler.on_new_line(line) # uses the handler callback for the new line # Send a multiline script def send_script(self, script): - print("Sending script: ") + self.logger.info("Sending script") script = script.split("\n") for s in script: - print("> " + s) if s != "" and s != " ": self.send_gcode_command(s) def reset_line_number(self, line_number = 2): - print("Resetting line number") + self.logger.info("Resetting line number") self.send_gcode_command("M110 N{}".format(line_number)) def serial_ports_list(self): diff --git a/UIserver/hw_controller/feeder_event_manager.py b/UIserver/hw_controller/feeder_event_manager.py index 02dd1f13..3e9a0c3c 100644 --- a/UIserver/hw_controller/feeder_event_manager.py +++ b/UIserver/hw_controller/feeder_event_manager.py @@ -6,7 +6,6 @@ def __init__(self, app): self.app = app def on_drawing_ended(self, code): - print("S> Sending drawing ended") self.app.logger.info("B> Drawing ended") self.app.semits.show_toast_on_UI("Drawing ended") self.app.semits.send_nav_drawing_status() diff --git a/UIserver/utils/settings_utils.py b/UIserver/utils/settings_utils.py index fb7e1d3a..f48a8469 100644 --- a/UIserver/utils/settings_utils.py +++ b/UIserver/utils/settings_utils.py @@ -1,7 +1,14 @@ import shutil import os import json +import logging + +# Logging levels (see the documentation of the logging module for more details) +LINE_SENT = 6 +LINE_RECEIVED = 5 + +# settings paths settings_path = "./UIserver/saves/saved_settings.json" defaults_path = "UIserver/saves/default_settings.json" @@ -17,7 +24,7 @@ def load_settings(): return settings def update_settings_file_version(): - print("Updating settings save files") + logging.info("Updating settings save files") if(not os.path.exists(settings_path)): shutil.copyfile(defaults_path, settings_path) else: @@ -41,6 +48,28 @@ def match_dict(mod_dict, ref_dict): new_dict[k] = ref_dict[k] return new_dict +# print the level of the logger selected +def print_level(level, logger_name): + description = "" + if level < LINE_RECEIVED: + description = "NOT SET" + elif level < LINE_SENT: + description = "LINE_RECEIVED" + elif level < 10: + description = "LINE_SENT" + elif level < 20: + description = "DEBUG" + elif level < 30: + description = "INFO" + elif level < 40: + description = "WARNING" + elif level < 50: + description = "ERROR" + else: + description = "CRITICAL" + print("Logger '{}' level: {} ({})".format(logger_name, level, description)) + + if __name__ == "__main__": # testing update_settings_file_version settings_path = "../"+settings_path diff --git a/UIserver/views/drawings_management.py b/UIserver/views/drawings_management.py index 067fb6ee..4212eebe 100644 --- a/UIserver/views/drawings_management.py +++ b/UIserver/views/drawings_management.py @@ -153,8 +153,6 @@ def delete_playlist(code): # Show queue @app.route('/queue') def show_queue(): - status = app.feeder.get_status() # TODO pass status to the template - print(status) code = app.qmanager.get_code() if not code is None: item = db.session.query(UploadedFiles).filter_by(id=code).first() From 91f58b8b7b50a1033d38fa38e7a55329e153dc20 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 11 Oct 2020 11:59:05 +0200 Subject: [PATCH 6/8] Fixed drawing queue order (#13) --- UIserver/hw_controller/feeder.py | 2 ++ UIserver/hw_controller/queue_manager.py | 2 +- UIserver/utils/buffered_timeout.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/UIserver/hw_controller/feeder.py b/UIserver/hw_controller/feeder.py index 65c3f6f4..406d3a8d 100644 --- a/UIserver/hw_controller/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -102,6 +102,7 @@ def connect(self): try: self.serial = DeviceSerial(settings['serial']['port'], settings['serial']['baud'], logger_name = __name__) self._serial_read_thread = Thread(target = self._thsr, daemon=True) + self._serial_read_thread.name = "serial_read" self._serial_read_thread.start() except: self.logger.info("Error during device connection") @@ -133,6 +134,7 @@ def start_code(self, code, force_stop=False): time.sleep(5) # wait a little for the thread to stop with self.serial_mutex: self._th = Thread(target = self._thf, args=(code,), daemon=True) + self._th.name = "drawing_feeder" self._isrunning = True self._ispaused = False self._running_code = code diff --git a/UIserver/hw_controller/queue_manager.py b/UIserver/hw_controller/queue_manager.py index a82f4d4f..be832663 100644 --- a/UIserver/hw_controller/queue_manager.py +++ b/UIserver/hw_controller/queue_manager.py @@ -68,7 +68,7 @@ def start_next(self, force_stop=False): if not force_stop: return False if self.queue_length() > 0: - self.start_drawing(self.q.queue.pop()) + self.start_drawing(self.q.queue.popleft()) self.app.logger.info("Starting next code") return True return False diff --git a/UIserver/utils/buffered_timeout.py b/UIserver/utils/buffered_timeout.py index a8a31365..f79ebabd 100644 --- a/UIserver/utils/buffered_timeout.py +++ b/UIserver/utils/buffered_timeout.py @@ -6,6 +6,7 @@ class BufferTimeout(Thread): def __init__(self, timeout_delta, function, group=None, target=None, name=None, args=(), kwargs=None): super(BufferTimeout, self).__init__(group=group, target=target, name=name) + self.name = "buffered_timeout" self.timeout_delta = timeout_delta self.callback = function self.mutex = Lock() From edbe977e43022de69bf875e825836aaf18280759 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 11 Oct 2020 14:26:53 +0200 Subject: [PATCH 7/8] Added emulator for fake serial: now is using 'realistic' timing for the acks to the feeder --- UIserver/hw_controller/device_serial.py | 12 ++-- UIserver/hw_controller/emulator.py | 84 +++++++++++++++++++++++++ UIserver/hw_controller/feeder.py | 19 +++++- 3 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 UIserver/hw_controller/emulator.py diff --git a/UIserver/hw_controller/device_serial.py b/UIserver/hw_controller/device_serial.py index 271827a2..337aee1d 100644 --- a/UIserver/hw_controller/device_serial.py +++ b/UIserver/hw_controller/device_serial.py @@ -3,6 +3,7 @@ import time import sys import logging +from UIserver.hw_controller.emulator import Emulator # This class connect to a serial device # If the serial device request is not available it will create a virtual serial device @@ -16,6 +17,8 @@ def __init__(self, serialname = None, baudrate = None, logger_name = None): self.is_fake = False self._buffer = bytearray() self.echo = "" + self._emulator = Emulator() + try: args = dict( baudrate = self.baudrate, @@ -33,9 +36,7 @@ def __init__(self, serialname = None, baudrate = None, logger_name = None): def send(self, obj): if self.is_fake: - #print("Fake> " + str(obj)) - self.echo = obj - time.sleep(0.05) + self._emulator.send(obj) else: if self.serial.is_open: try: @@ -76,8 +77,5 @@ def readline(self): line = self.serial.readline() return line.decode(encoding='UTF-8') else: - if not self.echo == "": - echo = "ok" # sends "ok" as ack otherwise the feeder will stop sending buffered commands - self.echo = "" - return echo + return self._emulator.readline() return None diff --git a/UIserver/hw_controller/emulator.py b/UIserver/hw_controller/emulator.py new file mode 100644 index 00000000..eb530460 --- /dev/null +++ b/UIserver/hw_controller/emulator.py @@ -0,0 +1,84 @@ +import time, re, math +from collections import deque + +emulated_commands_with_delay = ["G0", "G00", "G1", "G01"] + +class Emulator(): + def __init__(self): + self.feedrate = 5000.0 + self.ack_buffer = deque() # used for the standard "ok" acks timing + self.message_buffer = deque() # used to emulate marlin response to special commands + self.last_time = time.time() + self.xr = re.compile("[X]([0-9.]+)($|\s)") + self.yr = re.compile("[Y]([0-9.]+)($|\s)") + self.fr = re.compile("[F]([0-9.]+)($|\s)") + self.last_x = 0.0 + self.last_y = 0.0 + + def get_x(self, line): + return float(self.xr.findall(line)[0][0]) + + def get_y(self, line): + return float(self.yr.findall(line)[0][0]) + + def _buffer_empty(self): + return len(self.ack_buffer)<1 + + def send(self, command): + if self._buffer_empty(): + self.last_time = time.time() + # TODO introduce the response for particular commands (like feedrate request, position request and others) + + # reset position for G28 command + if "G28" in command: + self.last_x = 0.0 + self.last_y = 0.0 + self.message_buffer.append("ok") + + # when receives a line calculate the time between the line received and when the ack must be sent back with the feedrate + if any(code in command for code in emulated_commands_with_delay): + # check if should update feedrate + f = self.fr.findall(command) + if len(f) > 0: + self.feedrate = float(f[0][0]) + + # get points coords + try: + x = self.get_x(command) + except: + x = self.last_x + try: + y = self.get_y(command) + except: + y = self.last_y + # calculate time + t = math.sqrt((x-self.last_x)**2 + (y-self.last_y)**2) / self.feedrate * 60.0 + if t == 0.0: + self.message_buffer.append("ok") + return + + # update positions + self.last_x = x + self.last_y = y + + # add calculated time + self.last_time += t + self.ack_buffer.append(self.last_time) + + else: + self.message_buffer.append("ok") + + def readline(self): + # special commands response + if len(self.message_buffer) >= 1: + return self.message_buffer.popleft() + + # standard lines acks (G0, G1) + if self._buffer_empty(): + return None + oldest = self.ack_buffer.popleft() + if oldest > time.time(): + self.ack_buffer.appendleft(oldest) + return None + else: + return "ok" \ No newline at end of file diff --git a/UIserver/hw_controller/feeder.py b/UIserver/hw_controller/feeder.py index 406d3a8d..1d0935c7 100644 --- a/UIserver/hw_controller/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -6,6 +6,7 @@ import json from collections import deque from copy import deepcopy +import re import logging from dotenv import load_dotenv @@ -79,6 +80,10 @@ def __init__(self, handler = None, **kargvs): self.serial = None self.line_number = 0 self._timeout_last_line = self.line_number + self.feedrate = 0 + + # commands parser + self.feed_regex = re.compile("[F]([0-9.]+)($|\s)") # buffer control attrs self.command_buffer = deque() @@ -114,6 +119,7 @@ def connect(self): # reset line number when connecting self.reset_line_number() + self.request_feedrate() # send the "on connection" script from the settings self.send_script(settings['scripts']['connection']) @@ -213,6 +219,7 @@ def _thf(self, code): # thread that keep reading the serial port def _thsr(self): + line = None while True: with self.serial_mutex: try: @@ -288,6 +295,10 @@ def parse_device_line(self, line): self.logger.error("No line was found for the number required. Restart numeration.") self.send_gcode_command("M110 N1") + # TODO check feedrate response for M220 and set feedrate + #elif "_______" in line: # must see the real output from marlin + # self.feedrate = .... # must see the real output from marlin + elif "echo:Unknown command:" in line: self.logger.error("Error: command not found. Can also be a communication error") @@ -334,6 +345,9 @@ def send_gcode_command(self, command): # check if the command is in the "BUFFERED_COMMANDS" list and stops if the buffer is full if any(code in command for code in BUFFERED_COMMANDS): + if "F" in command: + feed = self.feed_regex.findall(command) + self.feedrate = feed[0][0] with self.command_send_mutex: # wait until get some "ok" command to remove an element from the buffer pass @@ -355,7 +369,7 @@ def send_gcode_command(self, command): self._update_timeout() # update the timeout because a new command has been sent with self.command_buffer_mutex: - if(len(self.command_buffer)>=self.command_buffer_max_length): + if(len(self.command_buffer)>=self.command_buffer_max_length and not self.command_send_mutex.locked()): self.command_send_mutex.acquire() # if the buffer is full acquire the lock so that cannot send new lines until the reception of an ack. Notice that this will stop only buffered commands. The other commands will be sent anyway self.handler.on_new_line(line) # uses the handler callback for the new line @@ -371,6 +385,9 @@ def send_script(self, script): def reset_line_number(self, line_number = 2): self.logger.info("Resetting line number") self.send_gcode_command("M110 N{}".format(line_number)) + + def request_feedrate(self): + self.send_gcode_command("M220") def serial_ports_list(self): result = self.serial.serial_port_list() From 51905ec010c789f95a5c84fd935810eb34109178 Mon Sep 17 00:00:00 2001 From: texx00 Date: Sat, 31 Oct 2020 10:58:04 +0100 Subject: [PATCH 8/8] Import fix for raspberry --- UIserver/hw_controller/device_serial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/UIserver/hw_controller/device_serial.py b/UIserver/hw_controller/device_serial.py index 337aee1d..7109273e 100644 --- a/UIserver/hw_controller/device_serial.py +++ b/UIserver/hw_controller/device_serial.py @@ -3,6 +3,7 @@ import time import sys import logging +import glob from UIserver.hw_controller.emulator import Emulator # This class connect to a serial device