diff --git a/.env.template b/.env.template index 3418f85f..468d018c 100644 --- a/.env.template +++ b/.env.template @@ -4,5 +4,9 @@ FLASK_ENV=development FLASK_DEBUG=0 -SHOW_FEEDER_TERMINAL=true -RUN_FEEDER_MANUALLY=true \ No newline at end of file + +# 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/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..6e41021f 100644 --- a/UIserver/__init__.py +++ b/UIserver/__init__.py @@ -2,24 +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 UIserver.bot_interface.queue_manager import QueueManager +from dotenv import load_dotenv +import logging + import sass from flask_minify import minify -from utils import settings_utils, software_updates +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.utils import settings_utils, software_updates + + + +# 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.logger.setLevel(logging.INFO) app.config['SECRET_KEY'] = 'secret!' # TODO put a key here app.config['UPLOAD_FOLDER'] = "./UIserver/static/Drawings" @@ -31,16 +51,25 @@ 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) 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 +from UIserver.sockets_interface.socketio_emits import SocketioEmits + +app.semits = SocketioEmits(app,socketio, db) + +# 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 +99,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/UIserver/hw_controller/device_serial.py b/UIserver/hw_controller/device_serial.py new file mode 100644 index 00000000..7109273e --- /dev/null +++ b/UIserver/hw_controller/device_serial.py @@ -0,0 +1,82 @@ +import serial.tools.list_ports +import serial +import time +import sys +import logging +import glob +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 + + +class DeviceSerial(): + 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 + self._buffer = bytearray() + self.echo = "" + self._emulator = Emulator() + + try: + args = dict( + baudrate = self.baudrate, + timeout = 0, + write_timeout = 0 + ) + self.serial = serial.Serial(**args) + self.serial.port = self.serialname + self.serial.open() + self.logger.info("Serial device connected") + except: + #print(traceback.print_exc()) + self.is_fake = True + self.logger.error("Serial not available. Will use the fake serial") + + def send(self, obj): + if self.is_fake: + self._emulator.send(obj) + else: + if self.serial.is_open: + try: + while self.readline(): + pass + self.serial.write(str(obj).encode()) + except: + self.close() + self.logger.error("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() + self.logger.info("Serial port closed") + except: + self.logger.error("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: + 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/NCFeeder/feeder.py b/UIserver/hw_controller/feeder.py similarity index 64% rename from NCFeeder/feeder.py rename to UIserver/hw_controller/feeder.py index e8261815..1d0935c7 100644 --- a/NCFeeder/feeder.py +++ b/UIserver/hw_controller/feeder.py @@ -1,27 +1,39 @@ 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 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 collections import deque from copy import deepcopy +import re + +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. + + +""" + +# 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 @@ -32,13 +44,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 @@ -51,45 +79,53 @@ def __init__(self, handler = None, **kargvs): else: self.handler = handler 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() - 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): 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.name = "serial_read" 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() # 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']) 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 @@ -104,6 +140,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 @@ -111,7 +148,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): @@ -150,10 +187,10 @@ 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()), "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,17 +214,19 @@ def _thf(self, code): self.send_gcode_command(line) self.send_script(settings['scripts']['after']) - self.handler.on_drawing_ended() + self.stop() + self.handler.on_drawing_ended(code) # thread that keep reading the serial port def _thsr(self): + line = None while True: with self.serial_mutex: 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) @@ -197,10 +236,10 @@ 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: + with self.serial_mutex: self.serial.send(line) else: self._update_timeout() @@ -227,15 +266,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 @@ -256,20 +292,22 @@ 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 + + # 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: - 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): 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 @@ -307,7 +345,10 @@ 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 + 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 # send the command after parsing the content @@ -315,152 +356,42 @@ 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): + 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 + 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)) - -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 request_feedrate(self): + self.send_gcode_command("M220") - 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 serial_ports_list(self): + result = self.serial.serial_port_list() + return [] if result is None else result - 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) + def is_connected(self): + return self.serial.is_connected() diff --git a/UIserver/hw_controller/feeder_event_manager.py b/UIserver/hw_controller/feeder_event_manager.py new file mode 100644 index 00000000..3e9a0c3c --- /dev/null +++ b/UIserver/hw_controller/feeder_event_manager.py @@ -0,0 +1,27 @@ +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): + self.app.logger.info("B> Drawing ended") + self.app.semits.show_toast_on_UI("Drawing ended") + self.app.semits.send_nav_drawing_status() + 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) + self.app.semits.send_nav_drawing_status() + + 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 95% rename from UIserver/bot_interface/queue_manager.py rename to UIserver/hw_controller/queue_manager.py index 0ea3a080..be832663 100644 --- a/UIserver/bot_interface/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 @@ -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 53% rename from UIserver/bot_interface/socketio_callbacks.py rename to UIserver/sockets_interface/socketio_callbacks.py index a076e093..7bc3ae0f 100644 --- a/UIserver/bot_interface/socketio_callbacks.py +++ b/UIserver/sockets_interface/socketio_callbacks.py @@ -3,19 +3,8 @@ 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 -def show_toast_on_UI(message): - socketio.emit("message_toast", message) - -@socketio.on('connect') -def on_connect(): - nav_drawing_request() - #app.logger.info("Connected") - pass - - -# ---- Frontend callbacks ---- @socketio.on('message') def handle_message(message): @@ -40,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 @@ -83,54 +64,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..54bccc93 --- /dev/null +++ b/UIserver/sockets_interface/socketio_emits.py @@ -0,0 +1,40 @@ +import traceback +from UIserver.database import UploadedFiles +from flask import render_template + +class SocketioEmits(): + 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.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.emit("command_line_show", line) + + + # sends the last position to update the preview box + def update_hw_preview(self, 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): + 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/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 6757f7f8..c364fa19 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/utils/buffered_timeout.py b/UIserver/utils/buffered_timeout.py new file mode 100644 index 00000000..f79ebabd --- /dev/null +++ b/UIserver/utils/buffered_timeout.py @@ -0,0 +1,36 @@ +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.name = "buffered_timeout" + 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 62% rename from utils/settings_utils.py rename to UIserver/utils/settings_utils.py index fb7e1d3a..f48a8469 100644 --- a/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/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 b9d36583..4212eebe 100644 --- a/UIserver/views/drawings_management.py +++ b/UIserver/views/drawings_management.py @@ -2,8 +2,8 @@ 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.bot_interface.socketio_callbacks import add_to_playlist +from UIserver.utils.gcode_converter import gcode_to_image +from UIserver.sockets_interface.socketio_callbacks import add_to_playlist import traceback import datetime @@ -153,7 +153,6 @@ def delete_playlist(code): # Show queue @app.route('/queue') def show_queue(): - socketio.emit("bot_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..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 @@ -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 - socketio.emit("serial_port_list_request") + 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 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