From b7446ab5b03938233139d160218d0fa8019642aa Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 6 Jan 2022 20:20:36 +0100 Subject: [PATCH 01/52] Update issue templates Fixed "where to find" the version and added the branch used --- .github/ISSUE_TEMPLATE/bug_report.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 256651ce..e0059021 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,7 +27,8 @@ If applicable, add screenshots to help explain your problem. - server OS: [Windows, Raspbian OS] - server hardware [PC, Raspberry Pi 3B+, ....] - serial device firmware [Marling, Grbl, ...] + version - - Version [use `git rev-parse --short HEAD`] + - Version hash [can be seen from the settings page in the web interface] + - Branch [if different than the main stable branch] **Additional context** Add any other context about the problem here. From e0eb34d8195c67454d92c52c0ecc8e05fa51edf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jan 2022 11:59:26 +0000 Subject: [PATCH 02/52] Bump follow-redirects from 1.14.6 to 1.14.7 in /frontend Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.7. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.7) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 007e68a2..7b5c5512 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5170,9 +5170,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0: - version "1.14.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" - integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== for-in@^1.0.2: version "1.0.2" From 994742776ec15859885d6112baac1a8d3e3af1cc Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 15 Jan 2022 14:26:44 +0100 Subject: [PATCH 03/52] Refactoring python code with black --- server/__init__.py | 51 +++-- server/api/drawings.py | 18 +- server/database/elements_factory.py | 18 +- server/database/generic_playlist_element.py | 46 ++-- server/database/models.py | 105 +++++---- server/database/playlist_elements.py | 204 +++++++++++------- server/database/playlist_elements_tables.py | 39 ++-- server/preprocessing/drawing_creator.py | 25 ++- server/preprocessing/file_observer.py | 11 +- .../sockets_interface/socketio_callbacks.py | 104 ++++++--- server/sockets_interface/socketio_emits.py | 7 +- server/utils/buffered_timeout.py | 12 +- server/utils/custom_math.py | 5 +- server/utils/gcode_converter.py | 190 +++++++++------- server/utils/limited_size_dict.py | 8 +- server/utils/logging_utils.py | 9 +- server/utils/settings_utils.py | 61 +++--- server/utils/software_updates.py | 19 +- server/utils/stats.py | 28 +-- 19 files changed, 593 insertions(+), 367 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index dfe1d3f0..1347bc3c 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -24,7 +24,12 @@ settings_utils.update_settings_file_version() # Shows ipv4 adresses -print("\nTo run the server use 'ip:5000' in your browser with one of the following ip adresses: {}\n".format(str(get_ip4_addresses())), flush=True) +print( + "\nTo run the server use 'ip:5000' in your browser with one of the following ip adresses: {}\n".format( + str(get_ip4_addresses()) + ), + flush=True, +) # Logging setup load_dotenv() @@ -48,34 +53,44 @@ # app setup # is using the frontend build forlder for the static path -app = Flask(__name__, template_folder='templates', static_folder="../frontend/build", static_url_path="/") +app = Flask( + __name__, template_folder="templates", static_folder="../frontend/build", static_url_path="/" +) app.logger.setLevel(1) w_logger.addHandler(server_stream_handler) w_logger.addHandler(server_file_handler) -app.config['SECRET_KEY'] = 'secret!' # TODO put a key here -app.config['UPLOAD_FOLDER'] = "./server/static/Drawings" +app.config["SECRET_KEY"] = "secret!" # TODO put a key here +app.config["UPLOAD_FOLDER"] = "./server/static/Drawings" + +# increasing this number increases CPU usage but it may be necessary to be able to run leds in realtime (default should be 16) +Payload.max_decode_packets = 200 -Payload.max_decode_packets = 200 # increasing this number increases CPU usage but it may be necessary to be able to run leds in realtime (default should be 16) socketio = SocketIO(app, cors_allowed_origins="*") -CORS(app) # setting up cors for react +CORS(app) # setting up cors for react + - -@app.route('/Drawings/') +@app.route("/Drawings/") def base_static(filename): filename = secure_filename(filename) - return send_from_directory(app.root_path + app.config['UPLOAD_FOLDER'].replace("./server", "")+ "/{}/".format(filename), "{}.jpg".format(filename)) + return send_from_directory( + app.root_path + + app.config["UPLOAD_FOLDER"].replace("./server", "") + + "/{}/".format(filename), + "{}.jpg".format(filename), + ) + # database DATABASE_FILENAME = os.path.join("server", "database", "db", "database.db") dbpath = os.environ.get("DB_PATH") if not dbpath is None: file_path = os.path.join(dbpath, DATABASE_FILENAME) -else: +else: file_path = os.path.join(os.path.abspath(os.getcwd()), DATABASE_FILENAME) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+file_path -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + file_path +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db = SQLAlchemy(app) migrate = Migrate(app, db, include_object=migrations.include_object) @@ -100,7 +115,6 @@ def base_static(filename): # Device controller initialization app.feeder = Feeder(FeederEventManager(app)) -#app.feeder.connect() app.qmanager = QueueManager(app, socketio) # Buttons controller initialization @@ -115,20 +129,22 @@ def base_static(filename): # Stats manager app.smanager = StatsManager() + @app.context_processor def override_url_for(): return dict(url_for=versioned_url_for) + # Adds a version number to the static url to update the cached files when a new version of the software is loaded def versioned_url_for(endpoint, **values): - if endpoint == 'static': + if endpoint == "static": pass values["version"] = app.umanager.short_hash return url_for(endpoint, **values) # Home routes -@app.route('/') +@app.route("/") def home(): return send_from_directory(app.static_folder, "index.html") @@ -139,7 +155,8 @@ def run_post(): app.feeder.connect() app.lmanager.start() -th = Thread(target = run_post) + +th = Thread(target=run_post) th.name = "feeder_starter" th.start() @@ -147,5 +164,5 @@ def run_post(): # initializes the .gcode file observer on the autostart folder app.observer = GcodeObserverManager("./server/autodetect", logger=app.logger) -if __name__ == '__main__': +if __name__ == "__main__": socketio.run(app) diff --git a/server/api/drawings.py b/server/api/drawings.py index d39bced6..ee223efa 100644 --- a/server/api/drawings.py +++ b/server/api/drawings.py @@ -5,20 +5,22 @@ ALLOWED_EXTENSIONS = ["gcode", "nc", "thr"] + def allowed_file(filename): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS + # Upload route for the dropzone to load new drawings -@app.route('/api/upload/', methods=['GET','POST']) +@app.route("/api/upload/", methods=["GET", "POST"]) def api_upload(): if request.method == "POST": - if 'file' in request.files: - file = request.files['file'] - if file and file.filename!= '' and allowed_file(file.filename): + if "file" in request.files: + file = request.files["file"] + if file and file.filename != "" and allowed_file(file.filename): # create entry in the database and preview image - id = preprocess_drawing(file.filename, file) + drawing_id = preprocess_drawing(file.filename, file) # refreshing list of drawings for all the clients drawings_refresh() - return jsonify(id) - return jsonify(-1) \ No newline at end of file + return jsonify(drawing_id) + return jsonify(-1) diff --git a/server/database/elements_factory.py b/server/database/elements_factory.py index 0b594a59..9ae8b626 100644 --- a/server/database/elements_factory.py +++ b/server/database/elements_factory.py @@ -2,17 +2,24 @@ from server.database.generic_playlist_element import GenericPlaylistElement from server.database.playlist_elements_tables import PlaylistElements -class ElementsFactory(): + +class ElementsFactory: @classmethod def create_element_from_dict(cls, dict_val): if not type(dict_val) is dict: + raise ValueError("The argument must be a dict") - if 'element_type' in dict_val: - el_type = dict_val.pop("element_type") # remove element type. Should be already be choosen when using the class + if "element_type" in dict_val: + # removing the element type. Should be already be choosen when using the class + el_type = dict_val.pop("element_type") else: raise ValueError("the dictionary must contain an 'element_type'") - from server.database.playlist_elements import _get_elements_types # need to import here to avoid circular import + # need to import here to avoid circular import + from server.database.playlist_elements import ( + _get_elements_types, + ) + for elementClass in _get_elements_types(): if elementClass.element_type == el_type: return elementClass(**dict_val) @@ -27,9 +34,8 @@ def create_element_from_json(cls, json_str): def create_element_from_db(cls, item): if not isinstance(item, PlaylistElements): raise ValueError("Need a db item from a playlist elements table") - + res = GenericPlaylistElement.clean_dict(item.__dict__) tmp = res.pop("element_options") res = {**res, **json.loads(tmp)} return cls.create_element_from_dict(res) - \ No newline at end of file diff --git a/server/database/generic_playlist_element.py b/server/database/generic_playlist_element.py index 76d8cdf0..331cafe5 100644 --- a/server/database/generic_playlist_element.py +++ b/server/database/generic_playlist_element.py @@ -1,12 +1,3 @@ -import json - -from server.database.models import db - -UNKNOWN_PROGRESS = { - "eta": -1, # default is -1 -> ETA unknown - "units": "s" # ETA units -} - """ Base class for a playlist element When creating a new element type, should extend this base class @@ -30,23 +21,34 @@ NOTE: variable starting with "_" will not be saved in the database NOTE: must implement the element also in the frontend (follow the instructions at the beginning of the "Elements.js" file) """ -class GenericPlaylistElement(): + +import json + +from server.database.models import db + +UNKNOWN_PROGRESS = {"eta": -1, "units": "s"} # default is -1 -> ETA unknown # ETA units + + +class GenericPlaylistElement: element_type = None - + # --- base class methods that must be implemented/overwritten in the child class --- def __init__(self, element_type, **kwargs): self.element_type = element_type - self._pop_options = [] # list of fields that are column in the database and must be removed from the standard options (string column) - self.add_column_field("element_type") # need to pop the element_type from the final dict because this option is a column of the table + # list of fields that are column in the database and must be removed from the standard options (string column) + self._pop_options = [] + # need to pop the element_type from the final dict because this option is a column of the table + self.add_column_field("element_type") for v in kwargs: setattr(self, v, kwargs[v]) - + # if this method return None if will not run the element in the playlist # can override and return another element if necessary + # pylint: disable=unused-argument def before_start(self, queue_manager): return self - + # this methods yields a gcode command line to be executed # the element is considered finished after the last line is yield # if a None value is yield, the feeder will skip to the next iteration @@ -66,7 +68,7 @@ def get_progress(self, feedrate): def get_path_length_total(self): """Returns the total lenght of the path of the drawing""" return 0 - + # Returns the current partial path done (in [mm]) def get_path_lenght_done(self): """Returns the path lenght that has been done for the current drawing""" @@ -82,7 +84,7 @@ def _set_from_dict(self, values): setattr(self, k, values[k]) else: raise ValueError - + def get_dict(self): return GenericPlaylistElement.clean_dict(self.__dict__) @@ -92,7 +94,7 @@ def __str__(self): # add options that must be saved in a dedicated column insted of saving them inside the generic options of the element (like the element_type) def add_column_field(self, option): self._pop_options.append(option) - + def save(self, element_table): options = self.get_dict() # filter other pop options @@ -102,8 +104,12 @@ def save(self, element_table): kwargs = zip(self._pop_options, kwargs) kwargs = dict(kwargs) options = json.dumps(options) - db.session.add(element_table(element_options = options, **kwargs)) + db.session.add(element_table(element_options=options, **kwargs)) @classmethod def clean_dict(cls, val): - return {key:value for key, value in val.items() if not key.startswith('_') and not callable(key)} \ No newline at end of file + return { + key: value + for key, value in val.items() + if not key.startswith("_") and not callable(key) + } diff --git a/server/database/models.py b/server/database/models.py index 63bf199a..a944fc75 100644 --- a/server/database/models.py +++ b/server/database/models.py @@ -1,3 +1,10 @@ +# pylint: disable=E1101 +""" +Database models + +""" + + from datetime import datetime import json @@ -16,7 +23,11 @@ class IdsSequences(db.Model): @classmethod def get_incremented_id(cls, table): ret_value = 1 - res = db.session.query(IdsSequences).filter(IdsSequences.id_name==table.__table__.name).first() + res = ( + db.session.query(IdsSequences) + .filter(IdsSequences.id_name == table.__table__.name) + .first() + ) # check if a row for the table has already been created if res is None: # get highest id in the table @@ -24,7 +35,7 @@ def get_incremented_id(cls, table): # if table is empty start from 1 otherwise use max(id) + 1 if not res is None: ret_value = res.id + 1 - db.session.add(IdsSequences(id_name = table.__table__.name, last_value = ret_value)) + db.session.add(IdsSequences(id_name=table.__table__.name, last_value=ret_value)) db.session.commit() else: res.last_value += 1 @@ -32,49 +43,67 @@ def get_incremented_id(cls, table): ret_value = res.last_value return ret_value + # Gcode files table # Stores information about the single drawing class UploadedFiles(db.Model): - id = db.Column(db.Integer, db.Sequence("uploaded_id"), primary_key=True, autoincrement=True) # drawing code (use "sequence" to avoid using the same id for new drawings (this will create problems with the cached data on the frontend, showing an old drawing instead of the freshly uploaded one)) - filename = db.Column(db.String(80), unique=False, nullable=False) # gcode filename - up_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Creation timestamp - edit_date = db.Column(db.DateTime, default=datetime.utcnow) # last time the drawing was edited (to update: datetime.datetime.utcnow()) - last_drawn_date = db.Column(db.DateTime) # last time the drawing was used by the table: to update: (datetime.datetime.utcnow()) - path_length = db.Column(db.Float) # total path lenght - dimensions_info = db.Column(db.String(150), unique=False) # additional dimensions information as json string object + # drawing code (use "sequence" to avoid using the same id for new drawings (this will create problems with the cached data on the frontend, showing an old drawing instead of the freshly uploaded one)) + id = db.Column(db.Integer, db.Sequence("uploaded_id"), primary_key=True, autoincrement=True) + # gcode filename + filename = db.Column(db.String(80), unique=False, nullable=False) + # Creation timestamp + up_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + # last time the drawing was edited (to update: datetime.datetime.utcnow()) + edit_date = db.Column(db.DateTime, default=datetime.utcnow) + # last time the drawing was used by the table: to update: (datetime.datetime.utcnow()) + last_drawn_date = db.Column(db.DateTime) + # total path lenght + path_length = db.Column(db.Float) + # additional dimensions information as json string object + dimensions_info = db.Column(db.String(150), unique=False) def __repr__(self): - return '' % self.filename - + return "" % self.filename + def save(self): return db.session.commit() @classmethod def get_full_drawings_list(cls): return db.session.query(UploadedFiles).order_by(UploadedFiles.edit_date.desc()).all() - + @classmethod def get_random_drawing(cls): return db.session.query(UploadedFiles).order_by(func.random()).first() @classmethod - def get_drawing(cls, id): - return db.session.query(UploadedFiles).filter(UploadedFiles.id==id).first() + def get_drawing(cls, drawing_id): + return db.session.query(UploadedFiles).filter(UploadedFiles.id == drawing_id).first() + # move these imports here to avoid circular import in the GenericPlaylistElement -from server.database.playlist_elements_tables import create_playlist_table, delete_playlist_table, get_playlist_table_class +from server.database.playlist_elements_tables import ( + create_playlist_table, + delete_playlist_table, + get_playlist_table_class, +) from server.database.elements_factory import ElementsFactory from server.database.generic_playlist_element import GenericPlaylistElement # Playlist table # Keep track of all the playlists class Playlists(db.Model): - id = db.Column(db.Integer, primary_key=True) # id of the playlist + # id of the playlist + id = db.Column(db.Integer, primary_key=True) + # playlist name name = db.Column(db.String(80), unique=False, nullable=False, default="New playlist") - creation_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) # Creation timestamp - edit_date = db.Column(db.DateTime, default=datetime.utcnow) # Last time the playlist was edited (to update: datetime.datetime.utcnow()) - version = db.Column(db.Integer, default=0) # Incremental version number: +1 every time the playlist is saved - + # Creation timestamp + creation_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + # Last time the playlist was edited (to update: datetime.datetime.utcnow()) + edit_date = db.Column(db.DateTime, default=datetime.utcnow) + # Incremental version number: +1 every time the playlist is saved + version = db.Column(db.Integer, default=0) + def save(self): self.edit_date = datetime.utcnow() self.version += 1 @@ -86,13 +115,15 @@ def add_element(self, elements): if not isinstance(elements, list): elements = [elements] for i in elements: - if "id" in i: # delete old ids to mantain the new sorting scheme (the elements list should be already ordered, for this reason we clear the elements and add them in the right order) + # delete old ids to mantain the new sorting scheme + # (the elements list should be already ordered, for this reason we clear the elements and add them in the right order) + if "id" in i: del i["id"] if not isinstance(i, GenericPlaylistElement): i = ElementsFactory.create_element_from_dict(i) i.save(self._ec()) db.session.commit() - + def clear_elements(self): return self._ec().clear_elements() @@ -102,25 +133,27 @@ def get_elements(self): for e in els: res.append(ElementsFactory.create_element_from_db(e)) return res - + def get_elements_json(self): els = self.get_elements() return json.dumps([e.get_dict() for e in els]) def to_json(self): - return json.dumps({ - "name": self.name, - "elements": self.get_elements_json(), - "id": self.id, - "version": self.version - }) + return json.dumps( + { + "name": self.name, + "elements": self.get_elements_json(), + "id": self.id, + "version": self.version, + } + ) # returns the database table class for the elements of that playlist def _ec(self): if not hasattr(self, "_tc"): self._tc = get_playlist_table_class(self.id) return self._tc - + @classmethod def create_playlist(cls): item = Playlists() @@ -128,24 +161,24 @@ def create_playlist(cls): db.session.commit() create_playlist_table(item.id) return item - + @classmethod def get_playlist(cls, id): if id is None: raise ValueError("An id is necessary to select a playlist") try: - return db.session.query(Playlists).filter(Playlists.id==id).one() # todo check if there is at leas one line (if the playlist exist) + return db.session.query(Playlists).filter(Playlists.id == id).one() + # TODO check if there is at least one line (if the playlist exist) except: return Playlists.create_playlist() @classmethod - def delete_playlist(cls, id): - item = db.session.query(Playlists).filter_by(id=id).first() + def delete_playlist(cls, playlist_id): + item = db.session.query(Playlists).filter_by(id=playlist_id).first() db.session.delete(item) db.session.commit() - delete_playlist_table(id) + delete_playlist_table(playlist_id) - # The app is using Flask-migrate # When a modification is applied to the db structure (new table, table structure modification like column name change, new column etc.) diff --git a/server/database/playlist_elements.py b/server/database/playlist_elements.py index aa713880..4600353e 100644 --- a/server/database/playlist_elements.py +++ b/server/database/playlist_elements.py @@ -15,6 +15,7 @@ from server.utils.gcode_converter import ImageFactory from server.utils.settings_utils import load_settings, get_only_values + """ --------------------------------------------------------------------------- @@ -23,34 +24,43 @@ New elements must be added to the _get_elements_types list at the end of this file --------------------------------------------------------------------------- -""" +""" -""" - Identifies a drawing element -""" class DrawingElement(GenericPlaylistElement): + """ + Identifies a drawing element + + """ + element_type = "drawing" def __init__(self, drawing_id=None, **kwargs): - super(DrawingElement, self).__init__(element_type=DrawingElement.element_type, **kwargs) # define the element type - self.add_column_field("drawing_id") # the drawing id must be saved in a dedicated column to be able to query the database and find for example in which playlist the drawing is used + # define the element type + super(DrawingElement, self).__init__(element_type=DrawingElement.element_type, **kwargs) + + # the drawing id must be saved in a dedicated column to be able to query the database and find for example in which playlist the drawing is used + self.add_column_field("drawing_id") try: self.drawing_id = int(drawing_id) except: raise ValueError("The drawing id must be an integer") self._distance = 0 self._total_distance = 0 - self._new_position = DotMap({"x":0, "y":0}) + self._new_position = DotMap({"x": 0, "y": 0}) self._last_position = self._new_position - self._x_regex = re.compile("[X]([0-9.-]+)($|\s)") # looks for a +/- float number after an X, until the first space or the end of the line - self._y_regex = re.compile("[Y]([0-9.-]+)($|\s)") # looks for a +/- float number after an Y, until the first space or the end of the line + # regexs for a +/- float number after an X, until the first space or the end of the line + self._x_regex = re.compile("[X]([0-9.-]+)($|\s)") + # regex for a +/- float number after an Y, until the first space or the end of the line + self._y_regex = re.compile("[Y]([0-9.-]+)($|\s)") - def execute(self, logger): # generate filename - filename = os.path.join(str(Path(__file__).parent.parent.absolute()), "static/Drawings/{0}/{0}.gcode".format(self.drawing_id)) - + filename = os.path.join( + str(Path(__file__).parent.parent.absolute()), + "static/Drawings/{0}/{0}.gcode".format(self.drawing_id), + ) + # loads the total lenght of the drawing to calculate eta drawing_infos = UploadedFiles.get_drawing(self.drawing_id) self._total_distance = drawing_infos.path_length @@ -59,11 +69,12 @@ def execute(self, logger): # if no path lenght is available try to calculate it and save it again (necessary for old versions compatibility, TODO remove this in future versions?) # need to open the file an extra time to analyze it completely (cannot do it while executing the element) try: - with open(filename) as f: + with open(filename, encoding="utf-8") as f: settings = load_settings() factory = ImageFactory(get_only_values(settings["device"])) - dimensions, _ = factory.gcode_to_coords(f) # ignores the coordinates and use only the drawing dimensions - drawing_infos.path_length = dimensions["total_lenght"] + # ignores the coordinates and use only the drawing dimensions + dimensions, _ = factory.gcode_to_coords(f) + drawing_infos.path_length = dimensions["total_lenght"] del dimensions["total_lenght"] drawing_infos.dimensions_info = json.dumps(dimensions) drawing_infos.save() @@ -74,9 +85,9 @@ def execute(self, logger): with open(filename) as f: for line in f: # clears the line - if line.startswith(";"): # skips commented lines + if line.startswith(";"): # skips commented lines continue - if ";" in line: # remove in line comments + if ";" in line: # remove in line comments line.split(";") line = line[0] # calculates the distance travelled @@ -85,7 +96,10 @@ def execute(self, logger): self._new_position.x = float(self._x_regex.findall(line)[0][0]) if "Y" in line: self._new_position.y = float(self._y_regex.findall(line)[0][0]) - self._distance += sqrt((self._new_position.x - self._last_position.x)**2 + (self._new_position.y - self._last_position.y)**2) + self._distance += sqrt( + (self._new_position.x - self._last_position.x) ** 2 + + (self._new_position.y - self._last_position.y) ** 2 + ) self._last_position = copy.copy(self._new_position) except Exception as e: logger.exception(e) @@ -99,28 +113,25 @@ def get_progress(self, feedrate): # if a feedrate is available will use "s" otherwise will calculate the ETA as a percentage if feedrate <= 0: - return { - "eta": self._distance/self._total_distance * 100, - "units": "%" - } + return {"eta": self._distance / self._total_distance * 100, "units": "%"} else: - return { - "eta": (self._total_distance - self._distance)/feedrate, - "units": "s" - } - + return {"eta": (self._total_distance - self._distance) / feedrate, "units": "s"} + def get_path_length_total(self): """Returns the total lenght of the path of the drawing""" return self._total_distance - + def get_path_lenght_done(self): """Returns the path lenght that has been done for the current drawing""" return self._distance -""" - Identifies a command element (sends a specific command/list of commands to the board) -""" + class CommandElement(GenericPlaylistElement): + """ + Identifies a command element (sends a specific command/list of commands to the board) + + """ + element_type = "command" def __init__(self, command, **kwargs): @@ -133,10 +144,12 @@ def execute(self, logger): yield c -""" - Identifies a timing element (delay between drawings, next drawing at specific time of the day, repetitions, etc) -""" class TimeElement(GenericPlaylistElement): + """ + Identifies a timing element (delay between drawings, next drawing at specific time of the day, repetitions, etc) + + """ + element_type = "timing" # delay: wait the specified amount of seconds @@ -149,56 +162,68 @@ def __init__(self, delay=None, expiry_date=None, alarm_time=None, type="", **kwa self.alarm_time = alarm_time if alarm_time != "" else None self.type = type self._final_time = -1 - + def execute(self, logger): self._final_time = time() - if self.type == "alarm_type": # compare the actual hh:mm:ss to the alarm to see if it must run today or tomorrow + if ( + self.type == "alarm_type" + ): # compare the actual hh:mm:ss to the alarm to see if it must run today or tomorrow now = datetime.now() - midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) # get midnight and add the alarm time + # get midnight and add the alarm time + midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) alarm_time = datetime.strptime(self.alarm_time, "%H:%M:%S") - alarm = midnight + timedelta(hours = alarm_time.hour, minutes = alarm_time.minute, seconds = alarm_time.second) + alarm = midnight + timedelta( + hours=alarm_time.hour, minutes=alarm_time.minute, seconds=alarm_time.second + ) if alarm == now: return elif alarm < now: - alarm += timedelta(hours=24) # if the alarm is expired for today adds 24h + alarm += timedelta(hours=24) # if the alarm is expired for today adds 24h self._final_time = datetime.timestamp(alarm) if self.type == "expiry_date": - self._final_time = datetime.timestamp(datetime.strptime(self.expiry_date, "%Y-%m-%d %H:%M:%S.%f")) + self._final_time = datetime.timestamp( + datetime.strptime(self.expiry_date, "%Y-%m-%d %H:%M:%S.%f") + ) elif self.type == "delay": - self._final_time += float(self.delay) # store current time and applies the delay - else: # should not be the case because the check is done already in the constructore - return - + self._final_time += float(self.delay) # store current time and applies the delay + else: # should not be the case because the check is done already in the constructor + return + while True: - if time() >= self._final_time: # If the delay expires can break the while to start the next element + if ( + time() >= self._final_time + ): # If the delay expires can break the while to start the next element break - elif time() < self._final_time-1: - logger.log(LINE_RECEIVED, "Waiting {:.1f} more seconds".format(self._final_time-time())) + elif time() < self._final_time - 1: + logger.log( + LINE_RECEIVED, "Waiting {:.1f} more seconds".format(self._final_time - time()) + ) sleep(1) yield None - else: - sleep(self._final_time-time()) + else: + sleep(self._final_time - time()) yield None - + # updates the delay value # used when in continuous mode def update_delay(self, interval): - self._final_time += (float(interval - self.delay)) + self._final_time += float(interval - self.delay) self.delay = interval # return a progress only if the element is running def get_progress(self, feedrate): if self._final_time != -1: - return { - "eta": self._final_time - time(), - "units": "s" - } - else: return super().get_progress(feedrate) + return {"eta": self._final_time - time(), "units": "s"} + else: + return super().get_progress(feedrate) + -""" - Plays an element in the playlist with a random order -""" class ShuffleElement(GenericPlaylistElement): + """ + Plays an element in the playlist with a random order + + """ + element_type = "shuffle" def __init__(self, shuffle_type=None, playlist_id=None, **kwargs): @@ -211,9 +236,9 @@ def before_start(self, app): if self.shuffle_type == None or self.shuffle_type == "0": # select random drawing drawing = UploadedFiles.get_random_drawing() - if drawing is None: # there is no drawing to be played + if drawing is None: # there is no drawing to be played return None - element = DrawingElement(drawing_id = drawing.id) + element = DrawingElement(drawing_id=drawing.id) elif self.playlist_id != 0: # select a random drawing from the current playlist res = get_playlist_table_class(self.playlist_id).get_random_drawing_element() @@ -222,53 +247,76 @@ def before_start(self, app): element.was_random = True return element -""" - Starts another playlist -""" + class StartPlaylistElement(GenericPlaylistElement): + """ + Starts another playlist + + """ + element_type = "start_playlist" def __init__(self, playlist_id=None, **kwargs): - super(StartPlaylistElement, self).__init__(element_type=StartPlaylistElement.element_type, **kwargs) + super(StartPlaylistElement, self).__init__( + element_type=StartPlaylistElement.element_type, **kwargs + ) self.playlist_id = int(playlist_id) if playlist_id is not None else 0 - + def before_start(self, app): # needs to import here to avoid circular import issue from server.sockets_interface.socketio_callbacks import playlist_queue + playlist_queue(self.playlist_id) return None - # TODO implement also the other element types (execute method but also the frontend options) -""" - Controls the led lights -""" + class LightsControl(GenericPlaylistElement): + """ + Controls the led lights + + """ + element_type = "" - def __init__(self, **kwargs): super().__init__(element_type=LightsControl.element_type, **kwargs) -""" - Identifies a particular behaviour for the ball between drawings (like: move to the closest border, start from the center) (should put this as a drawing option?) -""" class PositioningElement(GenericPlaylistElement): + """ + Identifies a particular behaviour for the ball between drawings (like: move to the closest border, start from the center) (should put this as a drawing option?) + + """ + element_type = "positioning" + def __init__(self, **kwargs): super().__init__(element_type=PositioningElement.element_type, **kwargs) -""" - Identifies a "clear all" pattern (really necessary?) -""" + class ClearElement(GenericPlaylistElement): + """ + Identifies a "clear all" pattern (really necessary?) + + """ + element_type = "clear" def __init__(self, **kwargs): super().__init__(element_type=ClearElement.element_type, **kwargs) + def _get_elements_types(): - return [DrawingElement, TimeElement, CommandElement, ShuffleElement, StartPlaylistElement, PositioningElement, ClearElement, LightsControl] + return [ + DrawingElement, + TimeElement, + CommandElement, + ShuffleElement, + StartPlaylistElement, + PositioningElement, + ClearElement, + LightsControl, + ] diff --git a/server/database/playlist_elements_tables.py b/server/database/playlist_elements_tables.py index 088bac47..bcf12145 100644 --- a/server/database/playlist_elements_tables.py +++ b/server/database/playlist_elements_tables.py @@ -1,3 +1,5 @@ +# pylint: disable=E1101 + import sqlalchemy from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.sql import func @@ -21,32 +23,39 @@ def clear_elements(cls): res = db.session.query(cls).delete() db.session.commit() return res - + # get random drawing element from the playlist (for the shuffle element) @classmethod def get_random_drawing_element(cls): return cls.query.filter(cls.drawing_id.isnot(None)).order_by(func.random()).first() + # creates sqlalchemy base class with the addition of the custom class -Base = declarative_base(cls = PlaylistElements) +Base = declarative_base(cls=PlaylistElements) Base.query = db.session.query_property() def get_playlist_table_class(id): - if id is None: raise ValueError("A playlist id must be specified") - table_name = "_playlist_{}".format(id) # table name is prefix + table_id - + if id is None: + raise ValueError("A playlist id must be specified") + table_name = "_playlist_{}".format(id) # table name is prefix + table_id + # if table exist use autoload otherwise create the table table_exist = table_name in sqlalchemy.inspect(db.engine).get_table_names() class PTable(Base): - __tablename__ = table_name # table name + __tablename__ = table_name # table name # set table args to load existing table if possible - __table_args__ = {'extend_existing': True, 'autoload': table_exist, 'autoload_with': db.get_engine()} + __table_args__ = { + "extend_existing": True, + "autoload": table_exist, + "autoload_with": db.get_engine(), + } id = db.Column(db.Integer, primary_key=True) element_type = db.Column(db.String(10), default="") - drawing_id = db.Column(db.Integer, default = None) # drawing id added explicitely for possible queries - element_options = db.Column(db.String(1000), default="") # element options + # drawing id added explicitely for possible queries + drawing_id = db.Column(db.Integer, default=None) + element_options = db.Column(db.String(1000), default="") # element options # change class attrs manually to avoid getting a warning ("This declarative base already contains a class with the same class name and module name") PTable.__name__ = table_name @@ -58,16 +67,18 @@ class PTable(Base): return PTable -def create_playlist_table(id): + +def create_playlist_table(playlist_id): """ Create a table associated to a single playlist. The number of tables will be the same as the number of playlists. """ - p_class = get_playlist_table_class(id) + _ = get_playlist_table_class(playlist_id) + -def delete_playlist_table(id): +def delete_playlist_table(playlist_id): """ Delete a table associated to a single playlist. """ - p_class = get_playlist_table_class(id) - p_class.__table__.drop(db.get_engine()) \ No newline at end of file + p_class = get_playlist_table_class(playlist_id) + p_class.__table__.drop(db.get_engine()) diff --git a/server/preprocessing/drawing_creator.py b/server/preprocessing/drawing_creator.py index f5df0733..1d07a731 100644 --- a/server/preprocessing/drawing_creator.py +++ b/server/preprocessing/drawing_creator.py @@ -10,6 +10,7 @@ import os import shutil + def preprocess_drawing(filename, file): # TODO add support to thr files @@ -19,31 +20,31 @@ def preprocess_drawing(filename, file): # this workaround fixes issue #40. Should fix it through the UploadFiles model with the primary key autoincrement but at the moment it is not working id = IdsSequences.get_incremented_id(UploadedFiles) - - new_file = UploadedFiles(id = id, filename = filename) + + new_file = UploadedFiles(id=id, filename=filename) db.session.add(new_file) db.session.commit() factory = ImageFactory(settings_utils.get_only_values(settings["device"])) # create a folder for each drawing. The folder will contain the .gcode file, the preview and additionally some settings for the drawing - folder = app.config["UPLOAD_FOLDER"] +"/" + str(new_file.id) +"/" + folder = app.config["UPLOAD_FOLDER"] + "/" + str(new_file.id) + "/" try: os.mkdir(folder) except: app.logger.error("The folder for '{}' already exists".format(new_file.id)) if hasattr(file, "save"): - file.save(os.path.join(folder, str(new_file.id)+".gcode")) + file.save(os.path.join(folder, str(new_file.id) + ".gcode")) else: - with open(os.path.join(folder, str(new_file.id)+".gcode"), "w") as f: + with open(os.path.join(folder, str(new_file.id) + ".gcode"), "w") as f: for l in file.readlines(): f.write(l) # create the preview image try: - with open(os.path.join(folder, str(new_file.id)+".gcode")) as file: + with open(os.path.join(folder, str(new_file.id) + ".gcode")) as file: dimensions, coords = factory.gcode_to_coords(file) image = factory.draw_image(coords, dimensions) # saving the new image - image.save(os.path.join(folder, str(new_file.id)+".jpg")) - + image.save(os.path.join(folder, str(new_file.id) + ".jpg")) + # saving additional information new_file.path_length = dimensions["total_lenght"] del dimensions["total_lenght"] @@ -52,9 +53,13 @@ def preprocess_drawing(filename, file): except: app.logger.error("Error during image creation") app.logger.error(traceback.print_exc()) - shutil.copy2(app.config["UPLOAD_FOLDER"]+"/placeholder.jpg", os.path.join(folder, str(new_file.id)+".jpg")) + shutil.copy2( + app.config["UPLOAD_FOLDER"] + "/placeholder.jpg", + os.path.join(folder, str(new_file.id) + ".jpg"), + ) + # TODO create a better placeholder? or add a routine to fix missing images? app.logger.info("File added") - return new_file.id \ No newline at end of file + return new_file.id diff --git a/server/preprocessing/file_observer.py b/server/preprocessing/file_observer.py index 36c97365..c7d7f28e 100644 --- a/server/preprocessing/file_observer.py +++ b/server/preprocessing/file_observer.py @@ -9,12 +9,13 @@ from server.preprocessing.drawing_creator import preprocess_drawing from server.sockets_interface.socketio_callbacks import drawings_refresh + class GcodeObserverManager: def __init__(self, path=".", logger=None): if logger is None: logger_name = __name__ self._logger = logging.getLogger(logger_name) - else: + else: self._logger = logger self._path = path self._observer = Observer() @@ -25,14 +26,14 @@ def __init__(self, path=".", logger=None): def start(self): self._observer.start() - + def stop(self): self._observer.stop() self._observer.join() def check_current_files(self): files = fnmatch.filter(os.listdir(self._path), "*.gcode") - if len(files)>0: + if len(files) > 0: self._logger.info("Found some files to load in the autodetect folder") for name in files: self._handler.init_drawing(os.path.join(self._path, name)) @@ -45,13 +46,13 @@ def __init__(self, logger): def on_created(self, evt): self.handle_event(evt) - + def on_moved(self, evt): self.handle_event(evt) def handle_event(self, evt): self.init_drawing(evt.src_path) - + def init_drawing(self, filename): self._logger.info("Uploading autodetected file: {}".format(filename)) try: diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index 5b6cc5b8..bc35adb8 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -1,3 +1,5 @@ +# pylint: disable=E1101 + import json import shutil import os @@ -9,22 +11,25 @@ from server.database.models import UploadedFiles, Playlists from server.database.playlist_elements import DrawingElement -@socketio.on('connect') + +@socketio.on("connect") def on_client_connected(): - app.qmanager.send_queue_status() # sending queue status - settings_request() # sending updated settings + app.qmanager.send_queue_status() # sending queue status + settings_request() # sending updated settings -# TODO split in multiple files? + +# TODO split in multiple files? # --------------------------------------------------------------- UPDATES ------------------------------------------------------------------------------------- -# request to check if a new version of the software is available -@socketio.on('updates_toggle_auto_enabled') +# request to check if a new version of the software is available +@socketio.on("updates_toggle_auto_enabled") def toggle_autoupdate_enabled(): app.umanager.toggle_autoupdate() settings_request() + # --------------------------------------------------------- PLAYLISTS CALLBACKS ------------------------------------------------------------------------------- # delete a playlist @@ -37,14 +42,19 @@ def playlist_delete(playlist_id): app.logger.error("'Delete playlist code {}' error".format(playlist_id)) playlist_refresh() + # save the changes to the playlist @socketio.on("playlist_save") def playlist_save(playlist): playlist = json.loads(playlist) - pl = Playlists.create_playlist() if ((not "id" in playlist) or (playlist["id"] == 0)) else Playlists.get_playlist(playlist['id']) + pl = ( + Playlists.create_playlist() + if ((not "id" in playlist) or (playlist["id"] == 0)) + else Playlists.get_playlist(playlist["id"]) + ) pl.clear_elements() - pl.name = playlist['name'] - pl.add_element(playlist['elements']) + pl.name = playlist["name"] + pl.add_element(playlist["elements"]) pl.save() app.logger.info("Playlist saved") playlist_refresh_single(pl.id) @@ -53,10 +63,10 @@ def playlist_save(playlist): # adds a playlist to the drawings queue @socketio.on("playlist_queue") def playlist_queue(code): - item = db.session.query(Playlists).filter(Playlists.id==code).one() + item = db.session.query(Playlists).filter(Playlists.id == code).one() elements = item.get_elements() for i in elements: - app.qmanager.queue_element(i, show_toast = False) + app.qmanager.queue_element(i, show_toast=False) @socketio.on("playlist_create_new") @@ -78,6 +88,7 @@ def playlist_refresh_single(playlist_id): playlist = db.session.query(Playlists).filter(Playlists.id == playlist_id).first() app.semits.emit("playlists_refresh_single_response", playlist.to_json()) + # --------------------------------------------------------- SETTINGS CALLBACKS ------------------------------------------------------------------------------- # settings callbacks @@ -93,25 +104,34 @@ def settings_save(data, is_connect): # updating feeder if is_connect: app.logger.info("Connecting 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("settings_request") def settings_request(): settings = settings_utils.load_settings() - settings["buttons"]["available_values"] = app.bmanager.get_buttons_options() - settings["buttons"]["available"] = app.bmanager.gpio_is_available() or int(os.getenv("DEV_HWBUTTONS", default="0")) - settings["leds"]["available"]["value"] = app.lmanager.is_available() or int(os.getenv("DEV_HWLEDS", default="0")) - settings["leds"]["has_light_sensor"]["value"] = app.lmanager.has_light_sensor() or int(os.getenv("DEV_HWLEDS", default="0")) - settings["serial"]["port"]["available_values"] = app.feeder.serial_ports_list() + settings["buttons"]["available_values"] = app.bmanager.get_buttons_options() + settings["buttons"]["available"] = app.bmanager.gpio_is_available() or int( + os.getenv("DEV_HWBUTTONS", default="0") + ) + settings["leds"]["available"]["value"] = app.lmanager.is_available() or int( + os.getenv("DEV_HWLEDS", default="0") + ) + settings["leds"]["has_light_sensor"]["value"] = app.lmanager.has_light_sensor() or int( + os.getenv("DEV_HWLEDS", default="0") + ) + settings["serial"]["port"]["available_values"] = app.feeder.serial_ports_list() settings["serial"]["port"]["available_values"].append("FAKE") - settings["updates"]["hash"] = app.umanager.short_hash - settings["updates"]["docker_compose_latest_version"] = app.umanager.docker_compose_latest_version - settings["updates"]["autoupdate"] = app.umanager.is_autoupdate_enabled() + settings["updates"]["hash"] = app.umanager.short_hash + settings["updates"][ + "docker_compose_latest_version" + ] = app.umanager.docker_compose_latest_version + settings["updates"]["autoupdate"] = app.umanager.is_autoupdate_enabled() tmp = [] labels = [v["label"] for v in settings["buttons"]["available_values"]] for b in settings["buttons"]["buttons"]: @@ -121,53 +141,62 @@ def settings_request(): settings["buttons"]["buttons"] = tmp app.semits.emit("settings_now", json.dumps(settings)) + @socketio.on("send_gcode_command") def send_gcode_command(command): app.feeder.send_gcode_command(command) + @socketio.on("settings_shutdown_system") def settings_shutdown_system(): app.semits.show_toast_on_UI("Shutting down the device") app.feeder.stop() app.lmanager.stop() - os.system("/sbin/shutdown now") # in order to shutdown inside a docker container + os.system("/sbin/shutdown now") # in order to shutdown inside a docker container + @socketio.on("settings_reboot_system") def settings_reboot_system(): app.semits.show_toast_on_UI("Rebooting system...") - os.system("/sbin/reboot") # in order to reboot inside a docker container + os.system("/sbin/reboot") # in order to reboot inside a docker container + # --------------------------------------------------------- DRAWINGS CALLBACKS ------------------------------------------------------------------------------- + @socketio.on("drawing_queue") def drawing_queue(code): element = DrawingElement(drawing_id=code) app.qmanager.reset_play_random() app.qmanager.queue_element(element) + @socketio.on("drawing_pause") def drawing_pause(): app.qmanager.pause() + @socketio.on("drawing_resume") def drawing_resume(): app.qmanager.resume() + @socketio.on("drawing_delete") def drawing_delete(code): item = db.session.query(UploadedFiles).filter_by(id=code).first() # TODO should delete the drawing also from every playlist - + try: if not item is None: db.session.delete(item) db.session.commit() - shutil.rmtree(app.config["UPLOAD_FOLDER"] +"/" + str(code) +"/") + shutil.rmtree(app.config["UPLOAD_FOLDER"] + "/" + str(code) + "/") app.logger.info("Drawing code {} deleted".format(code)) app.semits.show_toast_on_UI("Drawing deleted") except Exception as e: app.logger.error("'Delete drawing code {}' error".format(code)) + @socketio.on("drawings_refresh") def drawings_refresh(): rows = db.session.query(UploadedFiles).order_by(UploadedFiles.edit_date.desc()) @@ -179,25 +208,33 @@ def drawings_refresh(): # --------------------------------------------------------- QUEUE CALLBACKS ------------------------------------------------------------------------------- + @socketio.on("queue_get_status") def queue_get_status(): app.qmanager.send_queue_status() + @socketio.on("queue_set_order") def queue_set_order(elements): if elements == "": app.qmanager.clear_queue() else: - app.qmanager.set_new_order(map(lambda e: ElementsFactory.create_element_from_dict(e), json.loads(elements))) + app.qmanager.set_new_order( + map(lambda e: ElementsFactory.create_element_from_dict(e), json.loads(elements)) + ) + # stops only the current element @socketio.on("queue_next_drawing") def queue_next_drawing(): - app.semits.show_toast_on_UI("Stopping drawing...") + app.semits.show_toast_on_UI("Stopping drawing...") app.qmanager.start_next(force_stop=True) - if not app.qmanager.is_drawing(): # if the drawing was the last in the queue must send the updated status + if ( + not app.qmanager.is_drawing() + ): # if the drawing was the last in the queue must send the updated status app.qmanager.send_queue_status() + # clears the queue and stops the current element @socketio.on("queue_stop_all") def queue_stop_all(): @@ -206,34 +243,41 @@ def queue_stop_all(): queue_set_order("") app.qmanager.stop() + # sets the repeat flag for the queue @socketio.on("queue_set_repeat") def queue_set_repeat(val): app.qmanager.set_repeat(val) app.logger.info("repeat: {}".format(val)) + # sets the shuffle flag for the queue @socketio.on("queue_set_shuffle") def queue_set_shuffle(val): app.qmanager.set_shuffle(val) app.logger.info("shuffle: {}".format(val)) + # sets the queue interval @socketio.on("queue_set_interval") def queue_set_interval(val): app.qmanager.set_interval(float(val)) app.logger.info("interval: {}".format(val)) + # starts a random drawing from the uploaded list @socketio.on("queue_start_random") def queue_start_random(): app.qmanager.start_random_drawing(repeat=False) + # --------------------------------------------------------- LEDS CALLBACKS ------------------------------------------------------------------------------- + @socketio.on("leds_set_color") def leds_set_color(color): - app.lmanager.set_color(color) + app.lmanager.set_color(color) + @socketio.on("leds_auto_dim") def leds_set_autodim(val): @@ -243,8 +287,10 @@ def leds_set_autodim(val): else: app.lmanager.sensor.stop() + # --------------------------------------------------------- MANUAL CONTROL ------------------------------------------------------------------------------- + @socketio.on("control_emergency_stop") def control_emergency_stop(): - app.feeder.emergency_stop() \ No newline at end of file + app.feeder.emergency_stop() diff --git a/server/sockets_interface/socketio_emits.py b/server/sockets_interface/socketio_emits.py index 5eba2293..1c31c7f2 100644 --- a/server/sockets_interface/socketio_emits.py +++ b/server/sockets_interface/socketio_emits.py @@ -1,5 +1,4 @@ - -class SocketioEmits(): +class SocketioEmits: def __init__(self, app, socketio, db): self.app = app self.socketio = socketio @@ -9,16 +8,14 @@ def __init__(self, app, socketio, db): 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) # general emit def emit(self, topic, line): - self.socketio.emit(topic, line) \ No newline at end of file + self.socketio.emit(topic, line) diff --git a/server/utils/buffered_timeout.py b/server/utils/buffered_timeout.py index 38f23efd..de16fef8 100644 --- a/server/utils/buffered_timeout.py +++ b/server/utils/buffered_timeout.py @@ -1,10 +1,16 @@ 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): + """ + this thread calls a function after a timeout but only if the "update" method is not called before that timeout expires + + """ + + 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 @@ -13,7 +19,7 @@ def __init__(self, timeout_delta, function, group=None, target=None, name=None, self.is_running = False self.setDaemon(True) self.update() - + def set_timeout_period(self, val): self.timeout_delta = val diff --git a/server/utils/custom_math.py b/server/utils/custom_math.py index f7cf72ac..050abe86 100644 --- a/server/utils/custom_math.py +++ b/server/utils/custom_math.py @@ -1,4 +1,3 @@ - def multiply_tuple(tup, fval): - tup = tuple(i*fval for i in tup) - return tup \ No newline at end of file + tup = tuple(i * fval for i in tup) + return tup diff --git a/server/utils/gcode_converter.py b/server/utils/gcode_converter.py index 62aa214c..f49d57ea 100644 --- a/server/utils/gcode_converter.py +++ b/server/utils/gcode_converter.py @@ -2,27 +2,41 @@ from math import cos, sin, pi, sqrt from dotmap import DotMap + class ImageFactory: # straight lines gcode commands straight_lines = ["G01", "G1", "G0", "G00"] - # Args: - # - device: dict with the following values - # * type: device type (values: "Cartesian", "Polar", "Scara") - # * radius: for polar and scara needs the maximum radius of the device - # * offset_angle_1: for polar and scara needs an offset angle to rotate the view of the drawing (homing position angle) in motor units - # * offset_angle_2: for scara only: homing angle of the second part of the arm with respect to the first arm (alpha offset) in motor units - # * angle_conversion_factor (scara and polar): conversion value between motor units and radians (default for polar is pi, for scara is 6) - # - final_width (default: 800): final image width in px - # - final_height (default: 800): final image height in px - # - bg_color (default: (0,0,0)): tuple of the rgb color for the background - # - final_border_px (default: 20): the border to leave around the picture in px - # - line_width (default: 5): line thickness (px) - # - verbose (boolean) (default: False): if True prints the coordinates and other stuff in the command line - def __init__(self, device, final_width=800, final_height=800, bg_color=(0,0,0), line_color=(255,255,255), final_border_px=20, line_width=1, verbose=False): + def __init__( + self, + device, + final_width=800, + final_height=800, + bg_color=(0, 0, 0), + line_color=(255, 255, 255), + final_border_px=20, + line_width=1, + verbose=False, + ): + """ + Args: + - device: dict with the following values + * type: device type (values: "Cartesian", "Polar", "Scara") + * radius: for polar and scara needs the maximum radius of the device + * offset_angle_1: for polar and scara needs an offset angle to rotate the view of the drawing (homing position angle) in motor units + * offset_angle_2: for scara only: homing angle of the second part of the arm with respect to the first arm (alpha offset) in motor units + * angle_conversion_factor (scara and polar): conversion value between motor units and radians (default for polar is pi, for scara is 6) + - final_width (default: 800): final image width in px + - final_height (default: 800): final image height in px + - bg_color (default: (0,0,0)): tuple of the rgb color for the background + - final_border_px (default: 20): the border to leave around the picture in px + - line_width (default: 5): line thickness (px) + - verbose (boolean) (default: False): if True prints the coordinates and other stuff in the command line + """ self.final_width = final_width self.final_height = final_height - self.bg_color = bg_color if len(bg_color) == 4 else (*bg_color, 0) # color argument requires also alpha value + # color argument requires also alpha value + self.bg_color = bg_color if len(bg_color) == 4 else (*bg_color, 0) self.line_color = line_color self.final_border_px = final_border_px self.line_width = line_width @@ -35,12 +49,15 @@ def update_device(self, device): if self.is_scara(): # scara robot conversion factor # should be 2*pi/6 *1/2 (conversion between radians and motor units * 1/2 coming out of the theta alpha semplification) - self.pi_conversion = pi/float(device["angle_conversion_factor"]) # for scara robots follow https://forum.v1engineering.com/t/sandtrails-a-polar-sand-table/16844/61 + # for scara robots follow https://forum.v1engineering.com/t/sandtrails-a-polar-sand-table/16844/61 + self.pi_conversion = pi / float(device["angle_conversion_factor"]) self.device_radius = float(device["radius"]) - self.offset_1 = float(device["offset_angle_1"]) * 2 # *2 for the conversion factor (will spare one operation in the loop) - self.offset_2 = float(device["offset_angle_2"]) * 2 # *2 for the conversion factor (will spare one operation in the loop) + # *2 for the conversion factor (will spare one operation in the loop) + self.offset_1 = float(device["offset_angle_1"]) * 2 + # *2 for the conversion factor (will spare one operation in the loop) + self.offset_2 = float(device["offset_angle_2"]) * 2 elif self.is_polar(): - self.pi_conversion = 2.0*pi/float(device["angle_conversion_factor"]) + self.pi_conversion = 2.0 * pi / float(device["angle_conversion_factor"]) self.device_radius = float(device["radius"]) self.offset_1 = float(device["offset_angle_1"]) @@ -49,82 +66,86 @@ def is_cartesian(self): def is_polar(self): return self.device["type"] == "POLAR" - + def is_scara(self): return self.device["type"] == "SCARA" - # converts a gcode file to an image - # requires: gcode file (not filepath) - # return the image file def gcode_to_coords(self, file): + """ + converts a gcode file to an image + requires: gcode file (not filepath) + return the image file + + """ + total_lenght = 0 coords = [] - xmin = 100000 + xmin = 100000 xmax = -100000 - ymin = 100000 + ymin = 100000 ymax = -100000 old_X = 0 old_Y = 0 for line in file: # skipping comments - if line.startswith(";"): + if line.startswith(";"): continue - + # remove inline comments if ";" in line: line = line.split(";")[0] - if len(line) <3: + if len(line) < 3: continue # parsing line params = line.split(" ") - if not (params[0] in self.straight_lines): # TODO include also G2 and other curves command? - if(self.verbose): - print("Skipping line: "+line) + if not (params[0] in self.straight_lines): + # TODO include also G2 and other curves command? + if self.verbose: + print("Skipping line: " + line) continue - com_X = old_X # command X value - com_Y = old_Y # command Y value + com_X = old_X # command X value + com_Y = old_Y # command Y value # selecting values for p in params: - if p[0]=="X": + if p[0] == "X": com_X = float(p[1:]) - if p[0]=="Y": + if p[0] == "Y": com_Y = float(p[1:]) - + # calculates incremental lenght - total_lenght += sqrt(com_X**2 + com_Y**2) - + total_lenght += sqrt(com_X ** 2 + com_Y ** 2) + # converting command X and Y to x, y coordinates (default conversion is cartesian) x = com_X y = com_Y if self.is_scara(): # m1 = thehta+alpha - # m2 = theta-alpha - # -> + # m2 = theta-alpha + # -> # theta = (m1 + m2)/2 - # alpha = (m1-m2)/2 + # alpha = (m1-m2)/2 # (moving /2 into the pi_conversion to reduce the number of multiplications) theta = (com_X + com_Y + self.offset_1) * self.pi_conversion - rho = cos((com_X - com_Y + self.offset_2) * self.pi_conversion) * self.device_radius + rho = cos((com_X - com_Y + self.offset_2) * self.pi_conversion) * self.device_radius # calculate cartesian coords x = cos(theta) * rho - y = -sin(theta) * rho # uses - to remove preview mirroring + y = -sin(theta) * rho # uses - to remove preview mirroring elif self.is_polar(): - x = cos((com_X + self.offset_1)*self.pi_conversion) * com_Y * self.device_radius - y = sin((com_X + self.offset_1)*self.pi_conversion) * com_Y * self.device_radius + x = cos((com_X + self.offset_1) * self.pi_conversion) * com_Y * self.device_radius + y = sin((com_X + self.offset_1) * self.pi_conversion) * com_Y * self.device_radius - - if xxmax: + if x > xmax: xmax = x - if yymax: + if y > ymax: ymax = y - c = (x,y) + c = (x, y) coords.append(c) old_X = com_X old_Y = com_Y @@ -132,60 +153,71 @@ def gcode_to_coords(self, file): print("Coordinates:") print(coords) print("XMIN:{}, XMAX:{}, YMIN:{}, YMAX:{}".format(xmin, xmax, ymin, ymax)) + drawing_infos = { "total_lenght": total_lenght, "xmin": xmin, "xmax": xmax, "ymin": ymin, - "ymax": ymax + "ymax": ymax, } # return the image obtained from the coordinates return drawing_infos, coords - - # draws an image with the given coordinates (array of tuple of points) and the extremes of the points def draw_image(self, coords, drawing_infos): + """ + Draws an image with the given coordinates (array of tuple of points) and the extremes of the points + + """ limits = DotMap(drawing_infos) # Make the image larger than needed so can apply antialiasing factor = 5.0 - img_width = self.final_width*factor - img_height = self.final_height*factor - border_px = self.final_border_px*factor - image = Image.new('RGB', (int(img_width), int(img_height)), color=self.bg_color) + img_width = self.final_width * factor + img_height = self.final_height * factor + border_px = self.final_border_px * factor + image = Image.new("RGB", (int(img_width), int(img_height)), color=self.bg_color) d = ImageDraw.Draw(image) - rangex = limits.xmax-limits.xmin - rangey = limits.ymax-limits.ymin - scaleX = float(img_width - border_px*2)/rangex - scaleY = float(img_height - border_px*2)/rangey + rangex = limits.xmax - limits.xmin + rangey = limits.ymax - limits.ymin + scaleX = float(img_width - border_px * 2) / rangex + scaleY = float(img_height - border_px * 2) / rangey scale = min(scaleX, scaleY) def remapx(value): - return int((value-limits.xmin)*scale + border_px) - + return int((value - limits.xmin) * scale + border_px) + def remapy(value): - return int(img_height-((value-limits.ymin)*scale + border_px)) - + return int(img_height - ((value - limits.ymin) * scale + border_px)) + p_1 = coords[0] - self.circle(d, (remapx(p_1[0]), remapy(p_1[1])), self.line_width*factor/2, self.line_color) # draw a circle to make round corners - for p in coords[1:]: # create the line between two consecutive coordinates - d.line([remapx(p_1[0]), remapy(p_1[1]), remapx(p[0]), remapy(p[1])], \ - fill=self.line_color, width=int(self.line_width*factor)) + self.circle( + d, (remapx(p_1[0]), remapy(p_1[1])), self.line_width * factor / 2, self.line_color + ) # draw a circle to make round corners + for p in coords[1:]: # create the line between two consecutive coordinates + d.line( + [remapx(p_1[0]), remapy(p_1[1]), remapx(p[0]), remapy(p[1])], + fill=self.line_color, + width=int(self.line_width * factor), + ) if self.verbose: print("coord: {} _ {}".format(remapx(p_1[0]), remapy(p_1[1]))) p_1 = p - self.circle(d, (remapx(p_1[0]), remapy(p_1[1])), self.line_width*factor/2, self.line_color) # draw a circle to make round corners + self.circle( + d, (remapx(p_1[0]), remapy(p_1[1])), self.line_width * factor / 2, self.line_color + ) # draw a circle to make round corners # Resize the image to the final dimension to use antialiasing image = image.resize((int(self.final_width), int(self.final_height)), Image.ANTIALIAS) return image def circle(self, d, c, r, color): - d.ellipse([c[0]-r, c[1]-r, c[0]+r, c[1]+r], fill=color, outline=None) + d.ellipse([c[0] - r, c[1] - r, c[0] + r, c[1] + r], fill=color, outline=None) def thr_to_image(self, file): pass + if __name__ == "__main__": # testing scara device = { @@ -193,18 +225,16 @@ def thr_to_image(self, file): "angle_conversion_factor": 6.0, "radius": 200, "offset_angle": -1.5, - "offset_angle_2": 1.5 + "offset_angle_2": 1.5, } factory = ImageFactory(device, verbose=True) - with open('server/utils/test_scara.gcode') as file: + with open("server/utils/test_scara.gcode") as file: im = factory.gcode_to_image(file) im.show() - + # testing cartesian - device = { - "type": "Cartesian" - } + device = {"type": "Cartesian"} factory = ImageFactory(device, verbose=True) - with open('server/utils/test_cartesian.gcode') as file: + with open("server/utils/test_cartesian.gcode") as file: im = factory.gcode_to_image(file) - im.show() \ No newline at end of file + im.show() diff --git a/server/utils/limited_size_dict.py b/server/utils/limited_size_dict.py index 4d4679e3..08fe981e 100644 --- a/server/utils/limited_size_dict.py +++ b/server/utils/limited_size_dict.py @@ -1,9 +1,13 @@ 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): + """ + This dict class can have a size limit + Every time a new item is added to the dict, the oldest will be removed + + """ + def __init__(self, *args, **kwds): self.size_limit = kwds.pop("size_limit", None) OrderedDict.__init__(self, *args, **kwds) diff --git a/server/utils/logging_utils.py b/server/utils/logging_utils.py index 4bb22844..b7256f6a 100644 --- a/server/utils/logging_utils.py +++ b/server/utils/logging_utils.py @@ -16,18 +16,21 @@ def __init__(self, *kargs, **kwargs): # this is for sure not the best solution but it looks like it is working now def rotate(self, source, dest): shutil.copyfile(source, dest) - f = open(source, 'r+') + f = open(source, "r+") f.truncate(0) + # FIXME the rotating file handler is not working for some reason. should find a different solution. Create a new log file everytime the table is turned on? The file should be cached for some iterations? (5?) # create a common formatter for the app formatter = Formatter("[%(asctime)s] %(levelname)s in %(name)s (%(filename)s): %(message)s") -server_file_handler = MultiprocessRotatingFileHandler("server/logs/server.log", maxBytes=2000000, backupCount=5) +server_file_handler = MultiprocessRotatingFileHandler( + "server/logs/server.log", maxBytes=2000000, backupCount=5 +) server_file_handler.setLevel(1) server_file_handler.setFormatter(formatter) server_stream_handler = logging.StreamHandler() server_stream_handler.setLevel(logging.INFO) -server_stream_handler.setFormatter(formatter) \ No newline at end of file +server_stream_handler.setFormatter(formatter) diff --git a/server/utils/settings_utils.py b/server/utils/settings_utils.py index 2638f826..778b1f54 100644 --- a/server/utils/settings_utils.py +++ b/server/utils/settings_utils.py @@ -14,64 +14,62 @@ settings_path = "./server/saves/saved_settings.json" defaults_path = "./server/saves/default_settings.json" -OVERWRITE_FIELDS = [ - "available_values", - "depends_on", - "depends_values", - "tip", - "label" -] +OVERWRITE_FIELDS = ["available_values", "depends_on", "depends_values", "tip", "label"] + def save_settings(settings): dataj = json.dumps(settings, indent=4) - with open(settings_path,"w") as f: + with open(settings_path, "w") as f: f.write(dataj) + def load_settings(): settings = "" tmp_settings_path = settings_path - if not os.path.isfile(settings_path): # for python tests + if not os.path.isfile(settings_path): # for python tests tmp_settings_path = defaults_path with open(tmp_settings_path) as f: - settings = json.load(f) + settings = json.load(f) settings["system"] = {} settings["system"]["is_linux"] = platform.system() == "Linux" return settings - + + def update_settings_file_version(): logging.info("Updating settings save files") - if(not os.path.exists(settings_path)): + if not os.path.exists(settings_path): shutil.copyfile(defaults_path, settings_path) else: old_settings = load_settings() # compatibility check for older versions of the settings # older format of the settings is not compatible with the newer one, thus it must delete the settings. TODO Should remove this line after a while (I will give 3 months thus until 05/2021)... The first versions will not be installed on many devices - if not type(old_settings["serial"]["port"]) is dict: + if not type(old_settings["serial"]["port"]) is dict: shutil.copyfile(defaults_path, settings_path) - - + def_settings = "" with open(defaults_path) as f: def_settings = json.load(f) new_settings = match_dict(old_settings, def_settings) save_settings(new_settings) - + + def match_dict(mod_dict, ref_dict): if type(ref_dict) is dict: if not type(mod_dict) is dict: - return ref_dict # if the old field was not a dict but a single value must return the new dict because cannot convert a single value into a dict - - new_dict = dict(mod_dict) # clone object + return ref_dict # if the old field was not a dict but a single value must return the new dict because cannot convert a single value into a dict + + new_dict = dict(mod_dict) # clone object for k in ref_dict.keys(): if (not k in new_dict) or (k in OVERWRITE_FIELDS): - new_dict[k] = ref_dict[k] # if key is not set, adds the key as an empty dict + new_dict[k] = ref_dict[k] # if key is not set, adds the key as an empty dict else: new_dict[k] = match_dict(new_dict[k], ref_dict[k]) return new_dict else: return mod_dict + def get_only_values(ref_dict): res = {} if not type(ref_dict) is dict: @@ -84,8 +82,10 @@ def get_only_values(ref_dict): res[i] = get_only_values(ref_dict[i]) return res -# print the level of the logger selected + def print_level(level, logger_name): + """Print the level of the logger selected""" + description = "" if level < LINE_SERVICE: description = "NOT SET" @@ -107,12 +107,13 @@ def print_level(level, logger_name): description = "CRITICAL" print("Logger '{}' level: {} ({})".format(logger_name, level, description)) + def get_ip4_addresses(): ip_list = [] for interface in interfaces(): try: for link in ifaddresses(interface)[AF_INET]: - ip_list.append(link['addr']) + ip_list.append(link["addr"]) except: # if the interface is whitout ipv4 adresses can just pass pass @@ -123,19 +124,19 @@ def get_ip4_addresses(): # To run it must be in "$(env) server>" and use "python utils/settings_utils.py" if __name__ == "__main__": # testing update_settings_file_version - settings_path = "../"+settings_path - defaults_path = "../"+defaults_path + settings_path = "../" + settings_path + defaults_path = "../" + defaults_path - a = {"a":0, "b":{"c":2, "d":4}, "d":5} - b = {"a":1, "b":{"c":1, "e":5}, "c":3} - c = match_dict(a,b) + a = {"a": 0, "b": {"c": 2, "d": 4}, "d": 5} + b = {"a": 1, "b": {"c": 1, "e": 5}, "c": 3} + c = match_dict(a, b) print(a) print(b) print(c) - print(c=={"a":0, "b":{"c":2, "d":4, "e":5}, "d":5, "c":3}) + print(c == {"a": 0, "b": {"c": 2, "d": 4, "e": 5}, "d": 5, "c": 3}) update_settings_file_version() print(get_ip4_addresses()) - d = {"a":500, "b":{"asf":3, "value":10}, "c":{"d":{"fds":29, "value":32}}} - print(get_only_values(d)) \ No newline at end of file + d = {"a": 500, "b": {"asf": 3, "value": 10}, "c": {"d": {"fds": 29, "value": 32}}} + print(get_only_values(d)) diff --git a/server/utils/software_updates.py b/server/utils/software_updates.py index 0bcfdd90..8255828c 100644 --- a/server/utils/software_updates.py +++ b/server/utils/software_updates.py @@ -3,38 +3,45 @@ from dotenv import load_dotenv + def get_commit_shash(): res = {} with open("git_shash.json", "r") as f: res = json.load(f) return res["shash"] + # Checks if the docker compose file is at the latest version def check_docker_compose_latest_version(): load_dotenv() if not os.getenv("IS_DOCKER", default=None) is None: - return os.getenv("DOCKER_COMPOSE_FILE_VERSION") == os.getenv("DOCKER_COMPOSE_FILE_EXPECTED_VERSION") + return os.getenv("DOCKER_COMPOSE_FILE_VERSION") == os.getenv( + "DOCKER_COMPOSE_FILE_EXPECTED_VERSION" + ) return True + AUTOUPDATE_FILE_PATH = "./server/saves/autoupdate.txt" -class UpdatesManager(): + +class UpdatesManager: def __init__(self): self.short_hash = get_commit_shash() self.docker_compose_latest_version = check_docker_compose_latest_version() - + def autoupdate(self, enabled=True): if enabled and not self.is_autoupdate_enabled(): with open(AUTOUPDATE_FILE_PATH, "w"): pass if not enabled and self.is_autoupdate_enabled(): os.remove(AUTOUPDATE_FILE_PATH) - + def toggle_autoupdate(self): self.autoupdate(not self.is_autoupdate_enabled()) - + def is_autoupdate_enabled(self): return os.path.exists(AUTOUPDATE_FILE_PATH) + if __name__ == "__main__": - pass \ No newline at end of file + pass diff --git a/server/utils/stats.py b/server/utils/stats.py index 1850ce07..4379ede5 100644 --- a/server/utils/stats.py +++ b/server/utils/stats.py @@ -4,38 +4,41 @@ import os STATS_PATH = "./server/saves/stats.json" -SLEEP_TIME = 60 # will keep updating the "on_time" every n seconds +SLEEP_TIME = 60 # will keep updating the "on_time" every n seconds INIT_DICT = { - "last_on": 0.0, # last timestamp at wich the device was on [s] - "total_length": 0.0, # total lenght run by the sphere [mm] - "run_time": 0.0, # motors run time [s] - "on_time": 0.0 # total device on time [s] + "last_on": 0.0, # last timestamp at wich the device was on [s] + "total_length": 0.0, # total lenght run by the sphere [mm] + "run_time": 0.0, # motors run time [s] + "on_time": 0.0, # total device on time [s] } + def load_stats(): - if not os.path.isfile(STATS_PATH): + if not os.path.isfile(STATS_PATH): stats = INIT_DICT - else: + else: with open(STATS_PATH) as f: - stats = json.load(f) + stats = json.load(f) return stats + def save_stats(stats): dataj = json.dumps(stats, indent=4) with open(STATS_PATH, "w") as f: f.write(dataj) -class StatsManager(): + +class StatsManager: def __init__(self): self.stats = load_stats() self.stats["last_on"] = time() self.start_time = 0 self._mutex = Lock() - self._th = Thread(target = self._thf) + self._th = Thread(target=self._thf) self._th.name = "stats_manager" self._th.daemon = True self._th.start() - + def drawing_started(self): with self._mutex: self.start_time = time() @@ -61,6 +64,7 @@ def _thf(self): self._update_stats() sleep(SLEEP_TIME) + if __name__ == "__main__": sm = StatsManager() sm.drawing_started() @@ -68,4 +72,4 @@ def _thf(self): for i in range(WAIT_SECONDS): print(f"Waiting {WAIT_SECONDS-i} more seconds") sleep(1) - sm.drawing_ended(10.4) \ No newline at end of file + sm.drawing_ended(10.4) From 5f15c055e6ba1696f676ec6663cf1c375c3fca77 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Mon, 17 Jan 2022 22:46:38 +0100 Subject: [PATCH 04/52] Major queue manager refactoring Adding docs to the methods and simplifing/renaming stuff to make it simpler to understand --- server/database/playlist_elements.py | 3 +- server/hw_controller/buttons/actions.py | 20 +- server/hw_controller/feeder_event_manager.py | 11 +- server/hw_controller/queue_manager.py | 668 ++++++++++++------ .../sockets_interface/socketio_callbacks.py | 8 +- 5 files changed, 464 insertions(+), 246 deletions(-) diff --git a/server/database/playlist_elements.py b/server/database/playlist_elements.py index 4600353e..dfd5f3ca 100644 --- a/server/database/playlist_elements.py +++ b/server/database/playlist_elements.py @@ -244,7 +244,8 @@ def before_start(self, app): res = get_playlist_table_class(self.playlist_id).get_random_drawing_element() # convert the db element to the drawing element format element = GenericPlaylistElement.create_element_from_db(res) - element.was_random = True + # this is to keep track that the current element was generated by a shuffle element + element.was_shuffle = True return element diff --git a/server/hw_controller/buttons/actions.py b/server/hw_controller/buttons/actions.py index abfa490a..7f5506ea 100644 --- a/server/hw_controller/buttons/actions.py +++ b/server/hw_controller/buttons/actions.py @@ -1,12 +1,16 @@ from colorsys import hsv_to_rgb from random import random + # in this file are defined the events that can be associated to a button from server.hw_controller.buttons.generic_button_event import GenericButtonAction + class StartPause(GenericButtonAction): label = "Start/pause drawing" - description = "Resumes or pauses the current drawing. If nothing is in the queue starts a random drawing." + description = ( + "Resumes or pauses the current drawing. If nothing is in the queue starts a random drawing." + ) def execute(self): if self.app.qmanager.is_queue_empty(): @@ -14,7 +18,8 @@ def execute(self): else: if self.app.qmanager.is_paused(): self.app.qmanager.resume() - else: self.app.qmanager.pause() + else: + self.app.qmanager.pause() class StopAll(GenericButtonAction): @@ -23,6 +28,7 @@ class StopAll(GenericButtonAction): def execute(self): from server.sockets_interface.socketio_callbacks import queue_stop_all + queue_stop_all() @@ -32,6 +38,7 @@ class StartNext(GenericButtonAction): def execute(self): from server.sockets_interface.socketio_callbacks import queue_next_drawing + queue_next_drawing() @@ -52,7 +59,7 @@ class BrightnessDown(GenericButtonAction): def execute(self): self.app.lmanager.decrease_brightness() - + def tic(self, tic): self.execute() @@ -61,7 +68,7 @@ class BrightnessUpDown(GenericButtonAction): label = "Change LEDs brightness" description = "Changes LEDs brightness with a long press. After releasing the button the mode is toggled between ramp up and ramp down" usage = "long" - + def __init__(self, *args, **kargv): super().__init__(*args, **kargv) self.increasing = True @@ -88,7 +95,6 @@ class LEDsChangeColor(GenericButtonAction): description = "Chooses a random color for the LEDs" def execute(self): - rgb = hsv_to_rgb(random(),1,1) - c = [i*255 for i in rgb] + rgb = hsv_to_rgb(random(), 1, 1) + c = [i * 255 for i in rgb] self.app.lmanager.fill(c) - diff --git a/server/hw_controller/feeder_event_manager.py b/server/hw_controller/feeder_event_manager.py index 6a7bd4ab..b40a708d 100644 --- a/server/hw_controller/feeder_event_manager.py +++ b/server/hw_controller/feeder_event_manager.py @@ -2,6 +2,7 @@ from server.hw_controller.feeder import FeederEventHandler import time + class FeederEventManager(FeederEventHandler): def __init__(self, app): super().__init__() @@ -13,19 +14,21 @@ def on_element_ended(self, element): self.app.logger.info("Drawing ended") self.app.semits.show_toast_on_UI("Element ended") self.app.qmanager.set_element_ended() - self.app.smanager.drawing_ended(element.get_path_lenght_done()) # using path_lenght_done to take into account also the "stop drawing" cases + self.app.smanager.drawing_ended( + element.get_path_lenght_done() + ) # using path_lenght_done to take into account also the "stop drawing" cases if self.app.qmanager.is_queue_empty(): self.app.qmanager.send_queue_status() def on_element_started(self, element): - self.app.qmanager.set_element(element) + self.app.qmanager.element = element self.app.smanager.drawing_started() self.app.logger.info("Drawing started") self.app.semits.show_toast_on_UI("Element started") self.app.qmanager.send_queue_status() self.command_index = 0 self.last_send_time = time.time() - + def on_message_received(self, line): # Send the line to the server self.app.semits.hw_command_line_message(line) @@ -43,4 +46,4 @@ def on_new_line(self, line): def on_device_ready(self): self.app.qmanager.check_autostart() - self.app.qmanager.send_queue_status() \ No newline at end of file + self.app.qmanager.send_queue_status() diff --git a/server/hw_controller/queue_manager.py b/server/hw_controller/queue_manager.py index 03bf1373..d91e16eb 100644 --- a/server/hw_controller/queue_manager.py +++ b/server/hw_controller/queue_manager.py @@ -1,270 +1,478 @@ from queue import Queue -import json -from threading import Thread +from json import dumps +from threading import RLock, Thread import time -import random +from random import randrange +from dotmap import DotMap from server.utils import settings_utils from server.database.playlist_elements import ShuffleElement, TimeElement -TIME_CONVERSION_FACTOR = 60*60 # hours to seconds +TIME_CONVERSION_FACTOR = 60 * 60 # hours to seconds +QUEUE_UPDATE_INTERVAL = 30 # send the updated queue status every # seconds + + +class QueueStatusUpdater(Thread): + """ + Keep updating the queue status every given seconds + """ + + def __init__( + self, timeout, queue_manager, group=None, target=None, name=None, args=(), kwargs=None + ): + """ + Args: + timeout: the interval between the calls + queue_manager: the queue manager of which the status should be sent + """ + super(QueueStatusUpdater, self).__init__(group=group, target=target, name=name) + self.name = "queue_status_updater" + self.timeout = timeout + self.queue_manager = queue_manager + self.setDaemon(True) + + def run(self): + while True: + # updates the queue status every 30 seconds but only while is drawing + time.sleep(self.timeout) + if self.queue_manager.is_drawing(): + self.queue_manager.send_queue_status() + + +class QueueManager: + """ + This class manages the queue of elements that must be used + Can be filled one element at a time or by a playlist + """ -class QueueManager(): def __init__(self, app, socketio): - self._isdrawing = False - self._element = None - self.app = app - self.socketio = socketio - self.q = Queue() - self.repeat = False # true if should not delete the current element from the queue - self.shuffle = False # true if should shuffle the queue - self.interval = 0 # pause between drawing in repeat mode - self._last_time = 0 # timestamp of the end of the last drawing + """ + Args: + * app + * socketio istance + """ + # uses RLock to allow recursive call of functions + self.app = app + self.socketio = socketio + self._mutex = RLock() + + self.q = Queue() + self._element = None + # timestamp of the end of the last drawing + # used to understand if can start a new drawing or should put a delay element in between in the case an interval is choosen + self._last_time = 0 + # _is_force_stop is used to understand if the drawing_ended event was called because the drawing is ended or because of the stop button was used self._is_force_stop = False - self._play_random = False # True if the device was started with a "start a random drawing" commands + # play_random is "True" if the device was started with a "start a random drawing" commands + self._started_as_play_random_drawing = False - # setup status timer - self._th = Thread(target=self._thf, daemon=True) - self._th.name = "queue_status_interval" - self._th.start() - - def is_drawing(self): - return self._isdrawing + # queue controls status + self._controls = DotMap() + self._controls._repeat = False # true -> doesn't delete the current element from the queue + self._controls._shuffle = False # true -> shuffle the queue + self._controls._interval = 0.0 # pause between drawing in repeat mode - def is_paused(self): - return self.app.feeder.get_status()["is_paused"] + # status timer setup (keep updating the queue status every given seconds) + self._updater = QueueStatusUpdater(QUEUE_UPDATE_INTERVAL, self) + self._updater.start() - # pauses the feeder - def pause(self): - self.app.feeder.pause() - self.send_queue_status() - self.app.logger.info("Drawing paused") - - # resumes the feeder - def resume(self): - self.app.feeder.resume() - self.send_queue_status() - self.app.logger.info("Drawing resumed") + @property + def element(self): + """ + Returns: + the current element being drawn + """ + with self._mutex: + return self._element - # returns a boolean: true if the queue is empty and it is drawing, false otherwise - def is_queue_empty(self): - return not self._isdrawing and len(self.q.queue)==0 + @element.setter + def element(self, element): + """ + Set the current queue element - def set_is_drawing(self, dr): - self._isdrawing = dr + Args: + el: the element to use + """ + with self._mutex: + self.app.logger.info("Now running: {}".format(element)) + self._element = element - # returns the current element - def get_element(self): - return self._element - - # set the current element - def set_element(self, element): - self.app.logger.info("Now running: {}".format(element)) - self._element = element + @property + def repeat(self): + """ + The repeat option is used to keep a drawing in the queue. + If the value is False, at the end of the drawing the element is removed from the feeder queue. + If the value is True, the current drawing is put at the end of the queue - # stop the current drawing and start the next - def stop(self): - self._play_random = False - self._is_force_stop = True - self.app.feeder.stop() - - def reset_play_random(self): - self._play_random = False - - # set the repeat flag - def set_repeat(self, val): - if type(val) == type(True): - self.repeat = val - if val and (len(self.q.queue) > 0) and self._play_random: + Returns: + True if the repeat option is enabled, False otherwise + """ + with self._mutex: + return self._controls._repeat + + @repeat.setter + def repeat(self, val): + """ + Set the "repeat" value + + Args: + val: True if must keep the current drawing in the queue after is finished + + Raises: + ValueError: the argument must be boolean + """ + with self._mutex: + if not type(val) == type(True): + raise ValueError("The argument must be boolean") + self._controls._repeat = val + if val and (len(self.q.queue) > 0) and self._started_as_play_random_drawing: self._put_random_element_in_queue() self.send_queue_status() else: - if self._play_random: + if self._started_as_play_random_drawing: self.clear_queue() - self.reset_play_random() - else: - raise ValueError("The argument must be boolean") + self.reset_random_queue() - # set the shuffle flag - def set_shuffle(self, val): - if type(val) == type(True): - self.shuffle = val - else: raise ValueError("The argument must be boolean") + @property + def shuffle(self): + """ + The shuffle flag is used to play the elements in the queue in a random order - # set the queue interval [h] - def set_interval(self, val): - self.interval = val + Returns: + True if the elements in the queue are played in a random order + """ + with self._mutex: + return self._controls._shuffle - def _put_random_element_in_queue(self): - self.q.put(ShuffleElement(shuffle_type="0")) # queue a new random element drawing + @shuffle.setter + def shuffle(self, val): + """ + Args: + val: True to play the queue in a random order, False to follow the queue order + + Raises: + ValueError: the argument must be boolean + """ + with self._mutex: + if not (type(val) == type(True)): + raise ValueError("The argument must be boolean") + self._controls._shuffle = val + + @property + def interval(self): + """ + If the waiting time between drawings is different than 0, the queue manager will wait + the interval time before running the next element in the queue + Returns: + the waiting time between drawings. + """ + with self._mutex: + return self._controls._interval + + @interval.setter + def interval(self, interval): + """ + If an interval is set, a waiting time will be observed between elements + + Args: + interval: waiting time between drawings + """ + with self._mutex: + self._controls._interval = interval + + def is_queue_empty(self): + """ + Check if the queue is empty and the feeder is not running + + Returns: + True if the queue is empty and it is not drawing, False otherwise + """ + with self._mutex: + return not self.is_drawing() and len(self.q.queue) == 0 + + def is_paused(self): + """ + Check if the device is paused or not + + Returns: + True if the device is paused + """ + with self._mutex: + return self.app.feeder.get_status()["is_paused"] + + def is_drawing(self): + """ + Check if there is a drawing being done + + Returns: + True if there is a drawing running in the feeder + """ + with self._mutex: + return self.app.feeder.get_status()["is_running"] + + def pause(self): + """ + Pause the feeder + """ + with self._mutex: + self.app.feeder.pause() + self.send_queue_status() + self.app.logger.info("Drawing paused") + + def resume(self): + """ + Resume the feeder to the "drawing" status + """ + with self._mutex: + self.app.feeder.resume() + self.send_queue_status() + self.app.logger.info("Drawing resumed") + + def stop(self): + """ + Stop the current element + """ + with self._mutex: + self._started_as_play_random_drawing = False + self._is_force_stop = True + self.app.feeder.stop() + + def reset_random_queue(self): + """ + Stop the queue manager to keep on using random drawings + + This is necessary only when a manual change is done to the queue and the start command was given with the "play random drawing" button + """ + with self._mutex: + self._started_as_play_random_drawing = False + + def clear_queue(self): + """ + Clear the current queue + """ + with self._mutex: + self.q.queue.clear() + self.send_queue_status() + + def get_queue_len(self): + """ + Return the queue length + + Returns: + the queue length + """ + with self._mutex: + return self.q.qsize() - # starts a random drawing from the uploaded files def start_random_drawing(self, repeat=False): - self._play_random = True - self.set_shuffle(True) - if self.q.empty(): - self._put_random_element_in_queue() - else: - if not self.is_drawing(): - self._put_random_element_in_queue() - self.start_next() + """ + Start playing random drawings from the full uploaded list + Will work only if the queue is empty + Args: + repeat: True if should keep drawing after the current drawing is finished + Will set/reset the repeat flag, can be changed from the UI with the "repeat" button + """ + with self._mutex: + if self.is_queue_empty(): + # keep track that we started with a random drawing request + self._started_as_play_random_drawing = True + self.shuffle = True + self.repeat = repeat + self.clear_queue() + self._put_random_element_in_queue() + self.start_next() - # add an element to the queue def queue_element(self, element, show_toast=True): - if self.q.empty() and not self.is_drawing(): - self.start_element(element) - return - self.app.logger.info("Adding {} to the queue".format(element)) - self.q.put(element) - if show_toast: - self.app.semits.show_toast_on_UI("Element added to the queue") - self.send_queue_status() - - # return the content of the queue as a string - def queue_str(self): - return str(self.q.queue) - - def get_queue(self): - return self.q.queue + """ + Add an element to the queue + If the queue is empty, directly start the drawing + + Args: + element: the element to add/start + show_toast: (default) True if should show on the UI a toast that the drawing has been added to the queue + """ + with self._mutex: + # if the queue is empty, instead of adding the element to the queue, will start it directly + if self.is_queue_empty(): + self._start_element(element) + return + self.app.logger.info("Adding {} to the queue".format(element)) + self.q.put(element) # adding the element to the queue + if show_toast: # emitting the socket only if the show_toast flag is True + self.app.semits.show_toast_on_UI("Element added to the queue") + # refresh the queue status + self.send_queue_status() def set_element_ended(self): - self.set_is_drawing(False) - # if the ended element was forced to stop should not set the "last_time" otherwise when a new element is started there will be a delay element first - if self._is_force_stop: - self._is_force_stop = False - else: - self._last_time = time.time() - self.start_next() - - # clear the queue - def clear_queue(self): - self.q.queue.clear() - self.send_queue_status() - - def set_new_order(self, elements): - self.clear_queue() - for el in elements: - if el!= 0: - self.q.put(el) - self.send_queue_status() - - # remove the first element with the given code - def remove(self, code): - tmp = Queue() - is_first = True - for c in self.q.queue: - if c == code and is_first: - is_first = False + with self._mutex: + # if the ended element was forced to stop should not set the "last_time" otherwise when a new element is started there will be a delay element first + if self._is_force_stop: + # avoid setting the time and reset the flag + self._is_force_stop = False else: - tmp.put(c) - self.q = tmp - - # queue length - def queue_length(self): - return self.q.qsize() - - # start the next drawing of the queue - # by default will start it only if not already printing something - # with "force_stop = True" will stop the actual drawing and start the next + # the drawing was ended correctly (not stopped manually) and thus need to store the end time in case a delay element must be used + self._last_time = time.time() + # start the next element in the queue if necessary + self.start_next() + + def set_new_order(self, elements): + """ + Set the new queue order + + Args: + elements: list of elements with the correct order + """ + # Overwrite the queue completely thus first need to clear it completely + with self._mutex: + # avoid using the self.clear_queue in order not to send the status for nothing + self.q.queue.clear() + # fill the queue with the new elements + for el in elements: + if el != 0: + self.q.put(el) + # now can send back the new queue status + self.send_queue_status() + def start_next(self, force_stop=False): - if(self.is_drawing()): - if not force_stop: - return False - else: - # will reset the last_time to 0 in order to get the next element running without a delay and stop the current drawing. - # Once the current drawing the next drawing should start from the feeder event manager - self._last_time = 0 - self.stop() - return True - - try: - # should not remove the element from the queue if repeat is active. Should just add it at the end of the queue - if (not self._element is None) and (self.repeat) and (not hasattr(self._element, "_repeat_off")): - if hasattr(self._element, "was_random"): - self._put_random_element_in_queue() + """ + Start the next drawing in the queue + + By default will start only if not already drawing something + + Args: + force_stop: if True, force the current drawing/element stop and start the next one + + """ + with self._mutex: + if self.is_drawing(): + if not force_stop: + # if should not force the stop will exit the function + return else: - self.q.put(self._element) - - # if the time has not expired should start a new drawing otherwise should start a delay element - if (self.interval != 0) and (not hasattr(self._element, "_repeat_off") and (self.queue_length()>0)): - if (self._last_time + self.interval*TIME_CONVERSION_FACTOR > time.time()): - element = TimeElement(delay=self.interval*TIME_CONVERSION_FACTOR + time.time() - self._last_time, type="delay") - element._repeat_off = True # when the "repeat" flag is selected, should not add this element to the queue - self.start_element(element) - return True - - self._element = None - if self.queue_length() == 0: - return False - element = None - # if shuffle is enabled select a random drawing from the queue otherwise uses the first element of the queue - if self.shuffle: - tmp = None - elements = list(self.q.queue) - if len(elements)>1: # if the list is longer than 2 will pop the last element to avoid using it again - tmp = elements.pop(-1) - element = elements.pop(random.randrange(len(elements))) - elements.append(tmp) - self.set_new_order(elements) - else: - element = self.q.queue.popleft() - if element is None: - return False - # starts the choosen element - self.start_element(element) - self.app.logger.info("Starting next element: {}".format(element)) - return True - except Exception as e: - self.app.logger.exception(e) - self.app.logger.error("An error occured while starting a new drawing from the queue:\n{}".format(str(e))) - self.start_next() + # will reset the last_time to 0 in order to get the next element running without a delay and stop the current drawing. + # Once the current drawing the next drawing should start from the feeder event manager + self._last_time = 0 + self.stop() + return - # This method send a "start" command to the bot with the element - def start_element(self, element): - element = element.before_start(self.app) - if not element is None: - self.app.logger.info("Sending gcode start command") - self.set_is_drawing(True) - self.app.feeder.start_element(element, force_stop = True) - else: self.start_next() + try: + # should not remove the element from the queue if repeat is active. Should just add it back at the end of the queue + # avoid putting back interval delay element thanks to the "_repeat_off" property added when the delay element is created + if ( + (not self._element is None) + and (self.repeat) + and (not hasattr(self._element, "_repeat_off")) + ): + # check if the last element was generated by a "shuffle element" + # in that case must use again a shuffle element instead of the same element + if hasattr(self._element, "was_shuffle"): + self._put_random_element_in_queue() + else: + self.q.put(self._element) + + self._element = None + # if the queue is empty should just exit because there is no next element to start + if self.get_queue_len() == 0: + return + + # if the interval value is set and different than 0 may need to put a delay in between drawings + if self.interval != 0: + # add the interval to the timestap of the last drawing end and check if it bigger than the current timestamp to check if need to insert a delay element + if self._last_time + self.interval * TIME_CONVERSION_FACTOR > time.time(): + element = TimeElement( + delay=self.interval * TIME_CONVERSION_FACTOR + + time.time() + - self._last_time, + type="delay", + ) + # set a flag on the delay element to distinguish if it was created because there is a interval set + # the same flag is checked when the current element is check in order to add it back to the queue if the repeat flag is set + element._repeat_off = True + # if the element is a forced delay start it directly and exit the current method + self._start_element(element) + return + + next_element = None + # if shuffle is enabled select a random drawing from the queue otherwise uses the first element of the queue + if self.shuffle: + tmp = None + elements = list(self.q.queue) + if ( + len(elements) > 1 + ): # if the list is longer than 2 will pop the last element to avoid using it again + tmp = elements.pop(-1) + next_element = elements.pop(randrange(len(elements))) + elements.append(tmp) + self.set_new_order(elements) + else: + next_element = self.q.queue.popleft() + # start the choosen element + self._start_element(next_element) + self.app.logger.info("Starting next element: {}".format(next_element)) + + except Exception as e: + self.app.logger.exception(e) + self.app.logger.error( + "An error occured while starting a new drawing from the queue:\n{}".format( + str(e) + ) + ) + self.start_next() - # sends the queue status to the frontend def send_queue_status(self): - els = [i for i in self.q.queue if not i is None] - elements = list(map(lambda x: str(x), els)) if len(els) > 0 else [] # converts elements to json - res = { - "current_element": str(self._element), - "elements": elements, - "status": self.app.feeder.get_status(), - "repeat": self.repeat, - "shuffle": self.shuffle, - "interval": self.interval - } - self.app.semits.emit("queue_status", json.dumps(res)) - + """ + Send the queue status to the frontend + """ + with self._mutex: + els = [i for i in self.q.queue if not i is None] + elements = ( + list(map(lambda x: str(x), els)) if len(els) > 0 else [] + ) # converts elements to json + res = { + "current_element": str(self._element), + "elements": elements, + "status": self.app.feeder.get_status(), + "repeat": self.repeat, + "shuffle": self.shuffle, + "interval": self.interval, + } + self.app.semits.emit("queue_status", dumps(res)) + + def _start_element(self, element): + """ + Start the given element + """ + with self._mutex: + # check if a new element must be generated from the given element (like for a shuffle element) + element = element.before_start(self.app) + if not element is None: + self.app.logger.info("Sending gcode start command") + self.app.feeder.start_element(element, force_stop=True) + else: + self.start_next() + + def _put_random_element_in_queue(self): + """ + Queue a new random element from the full list of drawings + """ + with self._mutex: + # add the element to the queue without calling "queue_element" because this method is called also inside the start_next drawing and will be called twice + self.q.put(ShuffleElement(shuffle_type="0")) + + # TODO move this method into an "startup manager" which will need to initialize queue manager and initial status + # checks if should start drawing after the server is started and ready (can be set in the settings page) def check_autostart(self): - autostart = settings_utils.get_only_values(settings_utils.load_settings()["autostart"]) - - if autostart["on_ready"]: - self.start_random_drawing(repeat=True) - self.set_repeat(True) + with self._mutex: + autostart = settings_utils.get_only_values(settings_utils.load_settings()["autostart"]) - try: - if autostart["interval"]: - self.set_interval(float(autostart["interval"])) - except Exception as e: - self.app.logger.exception(e) + if autostart["on_ready"]: + self.start_random_drawing(repeat=True) + self.repeat = True - # periodically updates the queue status, used by the thread - def _thf(self): - while(True): - try: - # updates the queue status every 30 seconds but only while is drawing - time.sleep(30) - if self.is_drawing(): - self.send_queue_status() - - except Exception as e: - self.app.logger.exception(e) \ No newline at end of file + try: + if autostart["interval"]: + self.interval = float(autostart["interval"]) + except Exception as e: + self.app.logger.exception(e) diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index bc35adb8..96d1577a 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -167,7 +167,7 @@ def settings_reboot_system(): @socketio.on("drawing_queue") def drawing_queue(code): element = DrawingElement(drawing_id=code) - app.qmanager.reset_play_random() + app.qmanager.reset_random_queue() app.qmanager.queue_element(element) @@ -247,21 +247,21 @@ def queue_stop_all(): # sets the repeat flag for the queue @socketio.on("queue_set_repeat") def queue_set_repeat(val): - app.qmanager.set_repeat(val) + app.qmanager.repeat = val app.logger.info("repeat: {}".format(val)) # sets the shuffle flag for the queue @socketio.on("queue_set_shuffle") def queue_set_shuffle(val): - app.qmanager.set_shuffle(val) + app.qmanager.shuffle = val app.logger.info("shuffle: {}".format(val)) # sets the queue interval @socketio.on("queue_set_interval") def queue_set_interval(val): - app.qmanager.set_interval(float(val)) + app.qmanager.interval = float(val) app.logger.info("interval: {}".format(val)) From 11c48b9d3f67054e60b405324b7c1e2dea718708 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Jan 2022 11:43:29 +0000 Subject: [PATCH 05/52] Bump nanoid from 3.1.30 to 3.2.0 in /frontend Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.2.0. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.2.0) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 007e68a2..b8126a14 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7539,9 +7539,9 @@ nan@^2.12.1: integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanoid@^3.1.30: - version "3.1.30" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" - integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== nanomatch@^1.2.9: version "1.2.13" From e94836bb12843d452c4eebe223701969ba107211 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 23 Jan 2022 16:45:58 +0100 Subject: [PATCH 06/52] Started to divide feeder in multiple objects Now the feeder will manage different firmwares and estimators depending on the type of device used --- server/hw_controller/feeder_event_manager.py | 2 +- .../hw_controller/serial_device/__init__.py | 1 + .../{ => serial_device}/device_serial.py | 83 ++--- .../{ => serial_device}/emulator.py | 0 .../serial_device/estimation/__init__.py | 1 + .../serial_device/estimation/cartesian.py | 0 .../estimation/generic_estimator.py | 83 +++++ server/hw_controller/serial_device/feeder.py | 3 + .../serial_device/feeder_event_handler.py | 48 +++ .../serial_device/firmwares/__init__.py | 1 + .../firmwares/firmware_event_handler.py | 19 + .../firmwares/generic_firmware.py | 278 ++++++++++++++ .../serial_device/firmwares/marlin.py | 111 ++++++ .../{ => serial_device}/gcode_rescalers.py | 0 .../{ => serial_device/old}/feeder.py | 349 ++++++++++-------- .../old}/firmware_defaults.py | 0 16 files changed, 781 insertions(+), 198 deletions(-) create mode 100644 server/hw_controller/serial_device/__init__.py rename server/hw_controller/{ => serial_device}/device_serial.py (70%) rename server/hw_controller/{ => serial_device}/emulator.py (100%) create mode 100644 server/hw_controller/serial_device/estimation/__init__.py create mode 100644 server/hw_controller/serial_device/estimation/cartesian.py create mode 100644 server/hw_controller/serial_device/estimation/generic_estimator.py create mode 100644 server/hw_controller/serial_device/feeder.py create mode 100644 server/hw_controller/serial_device/feeder_event_handler.py create mode 100644 server/hw_controller/serial_device/firmwares/__init__.py create mode 100644 server/hw_controller/serial_device/firmwares/firmware_event_handler.py create mode 100644 server/hw_controller/serial_device/firmwares/generic_firmware.py create mode 100644 server/hw_controller/serial_device/firmwares/marlin.py rename server/hw_controller/{ => serial_device}/gcode_rescalers.py (100%) rename server/hw_controller/{ => serial_device/old}/feeder.py (70%) rename server/hw_controller/{ => serial_device/old}/firmware_defaults.py (100%) diff --git a/server/hw_controller/feeder_event_manager.py b/server/hw_controller/feeder_event_manager.py index b40a708d..f9678f6d 100644 --- a/server/hw_controller/feeder_event_manager.py +++ b/server/hw_controller/feeder_event_manager.py @@ -1,5 +1,5 @@ from server.database.playlist_elements import DrawingElement -from server.hw_controller.feeder import FeederEventHandler +from server.hw_controller.serial_device.feeder_event_handler import FeederEventHandler import time diff --git a/server/hw_controller/serial_device/__init__.py b/server/hw_controller/serial_device/__init__.py new file mode 100644 index 00000000..56fe7883 --- /dev/null +++ b/server/hw_controller/serial_device/__init__.py @@ -0,0 +1 @@ +# added this file to avoid linter errors/warnings diff --git a/server/hw_controller/device_serial.py b/server/hw_controller/serial_device/device_serial.py similarity index 70% rename from server/hw_controller/device_serial.py rename to server/hw_controller/serial_device/device_serial.py index 8a1cd65f..d7bbee0f 100644 --- a/server/hw_controller/device_serial.py +++ b/server/hw_controller/serial_device/device_serial.py @@ -1,44 +1,28 @@ -from enum import auto from threading import Thread, Lock import serial.tools.list_ports import serial -import time -import traceback import sys import logging -from server.hw_controller.emulator import Emulator import glob +from server.hw_controller.serial_device.emulator import Emulator + # This class connects 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 = 115200, logger_name = None, autostart = False): - self.logger = logging.getLogger(logger_name) if not logger_name is None else logging.getLogger() - self.serialname = serialname + +class DeviceSerial: + def __init__(self, serial_name=None, baudrate=115200, logger_name=None): + self.logger = ( + logging.getLogger(logger_name) if not logger_name is None else logging.getLogger() + ) + self.serialname = serial_name self.baudrate = baudrate self.is_fake = False self._buffer = bytearray() self.echo = "" self._emulator = Emulator() - # opening serial - 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 Exception as e: - self.logger.exception(e) - # TODO should add a check to see if the problem is that cannot use the Serial module because it is not installed correctly on raspberry - self.is_fake = True - self.logger.error("Serial not available. Are you sure the device is connected and is not in use by other softwares? (Will use the fake serial)") - # empty callback function def useless(arg): pass @@ -49,22 +33,33 @@ def useless(arg): self._th.name = "serial_read" self._running = False self.set_onreadline_callback(useless) - if autostart: - self.start_reading() - - # starts the reading thread - def start_reading(self): + def open(self): + # opening serial + 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 Exception as e: + # FIXME should check for different exceptions + self.logger.exception(e) + self.is_fake = True + self.logger.error( + "Serial not available. Are you sure the device is connected and is not in use by other softwares? (Will use the fake serial)" + ) + self._th.start() # this method is used to set a callback for the "new line available" event def set_onreadline_callback(self, callback): self._on_readline = callback - + # check if the reading thread is working def is_running(self): return self._running - + # stops the serial read thread def stop(self): self._running = False @@ -78,32 +73,32 @@ def send(self, obj): try: with self._mutex: while self.serial.out_waiting: - pass # TODO should add a sort of timeout + pass # TODO should add a sort of timeout self._readline() self.serial.write(str(obj).encode()) # TODO try to send byte by byte instead of a full line? (to reduce the risk of sending commands with missing digits or wrong values that may lead to a wrong position value) except: self.close() self.logger.error("Error while sending a command") - + # return a list of available serial ports def serial_port_list(self): - if sys.platform.startswith('win'): + 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'): + 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]*') + ports = glob.glob("/dev/tty[A-Za-z]*") else: - raise EnvironmentError('Unsupported platform') + raise EnvironmentError("Unsupported platform") return ports # check if is connected to a real device def is_connected(self): - if(self.is_fake): + if self.is_fake: return False return self.serial.is_open - + # close the connection with the serial device def close(self): self.stop() @@ -112,14 +107,14 @@ def close(self): self.logger.info("Serial port closed") except: self.logger.error("Error: serial already closed or not available") - + # private functions # reads a line from the device def _readline(self): if not self.is_fake: if self.serial.is_open: - while self.serial.in_waiting>0: + while self.serial.in_waiting > 0: line = self.serial.readline() return line.decode(encoding="UTF-8") else: @@ -129,8 +124,8 @@ def _readline(self): def _thf(self): self._running = True next_line = "" - while(self.is_running()): + while self.is_running(): with self._mutex: next_line = self._readline() # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method - self._on_readline(next_line) \ No newline at end of file + self._on_readline(next_line) diff --git a/server/hw_controller/emulator.py b/server/hw_controller/serial_device/emulator.py similarity index 100% rename from server/hw_controller/emulator.py rename to server/hw_controller/serial_device/emulator.py diff --git a/server/hw_controller/serial_device/estimation/__init__.py b/server/hw_controller/serial_device/estimation/__init__.py new file mode 100644 index 00000000..56fe7883 --- /dev/null +++ b/server/hw_controller/serial_device/estimation/__init__.py @@ -0,0 +1 @@ +# added this file to avoid linter errors/warnings diff --git a/server/hw_controller/serial_device/estimation/cartesian.py b/server/hw_controller/serial_device/estimation/cartesian.py new file mode 100644 index 00000000..e69de29b diff --git a/server/hw_controller/serial_device/estimation/generic_estimator.py b/server/hw_controller/serial_device/estimation/generic_estimator.py new file mode 100644 index 00000000..d9839a06 --- /dev/null +++ b/server/hw_controller/serial_device/estimation/generic_estimator.py @@ -0,0 +1,83 @@ +import re +from dotmap import DotMap + +# List of commands that can be parsed by the estimator +KNOWN_COMMANDS = ("G0", "G00", "G1", "G01", "G28", "G92") + +# TODO change this class to work in a different way +# every command should be represented by a function +# if the function is available will call the corresponding function in order to estimate the real trajectory/position + +# TODO should add realtime estimation in the "get_current_position" method instead of returning the one from the last command + + +class GenericEstimator: + """ + Keep track of the current position and extimate the time elapsed or the length of the path done + """ + + def __init__(self): + + self._position = DotMap({"x": 0, "y": 0}) + self._feedrate = 0 + self._path_length = 0 + + # regex generation for the parser + self._feed_regex = re.compile( + "[F]([0-9.-]+)($|\s)" + ) # looks for a +/- float number after an F, until the first space or the end of the line + self._x_regex = re.compile( + "[X]([0-9.-]+)($|\s)" + ) # looks for a +/- float number after an X, until the first space or the end of the line + self._y_regex = re.compile( + "[Y]([0-9.-]+)($|\s)" + ) # looks for a +/- float number after an Y, until the first space or the end of the line + + @property + def position(self): + """ + Returns: + estimated position of the device + """ + return self._position + + @position.setter + def position(self, pos): + """ + Set the position + + Args: + pos (dict): dict which must contain x and y coordinates + """ + if not (hasattr(pos, "x") and hasattr(pos, "y")): + raise ValueError("The position given must have both the x and y coordinates") + self._position = DotMap(pos) + + def get_last_commanded_position(self): + """ + Returns: + dict: last commanded x,y position + """ + # TODO this will change once the estimation is done properly + return self._position + + def reset_path_length(self): + self._path_length = 0 + + def parse_command(self, command): + """ + Parse buffer commands to get the position commanded or to reset position if is using G28/G92 + """ + # handling homing commands + if "G28" in command and (not "X" in command or "Y" in command): + self._position.x = 0 + self._position.y = 0 + # G92 is handled in the buffered commands + + if any(code in command for code in KNOWN_COMMANDS): + if "F" in command: + self.feedrate = float(self._feed_regex.findall(command)[0][0]) + if "X" in command: + self._position.x = float(self._x_regex.findall(command)[0][0]) + if "Y" in command: + self._position.y = float(self._y_regex.findall(command)[0][0]) diff --git a/server/hw_controller/serial_device/feeder.py b/server/hw_controller/serial_device/feeder.py new file mode 100644 index 00000000..54ade125 --- /dev/null +++ b/server/hw_controller/serial_device/feeder.py @@ -0,0 +1,3 @@ +class Feeder: + def __init__(self): + pass diff --git a/server/hw_controller/serial_device/feeder_event_handler.py b/server/hw_controller/serial_device/feeder_event_handler.py new file mode 100644 index 00000000..a1092751 --- /dev/null +++ b/server/hw_controller/serial_device/feeder_event_handler.py @@ -0,0 +1,48 @@ +class FeederEventHandler: + """ + Handle the event calls from the feeder + This is just a base class, every method is empty + Need to implement this in a custom event handler + """ + + def on_element_ended(self, element): + """ + Used when a drawing is finished + + Args: + element: the element that was ended + """ + pass + + def on_element_started(self, element): + """ + Used when a drawing is started + + Args: + element: the element that was started + """ + pass + + def on_message_received(self, line): + """ + Used when the device send a message that must be sent to the frontend + + Args: + line: the line received from the device + """ + pass + + def on_new_line(self, line): + """ + Used when a new line is passed to the device + + Args: + line: the line sent to the device + """ + pass + + def on_device_ready(self): + """ + Used when the connection with the device has been done and the device is ready + """ + pass diff --git a/server/hw_controller/serial_device/firmwares/__init__.py b/server/hw_controller/serial_device/firmwares/__init__.py new file mode 100644 index 00000000..56fe7883 --- /dev/null +++ b/server/hw_controller/serial_device/firmwares/__init__.py @@ -0,0 +1 @@ +# added this file to avoid linter errors/warnings diff --git a/server/hw_controller/serial_device/firmwares/firmware_event_handler.py b/server/hw_controller/serial_device/firmwares/firmware_event_handler.py new file mode 100644 index 00000000..480a4622 --- /dev/null +++ b/server/hw_controller/serial_device/firmwares/firmware_event_handler.py @@ -0,0 +1,19 @@ +from abc import abstractmethod, ABC + + +class FirwmareEventHandler(ABC): + """ + Event handler for line sent and received from the serial device + """ + + @abstractmethod + def on_line_sent(self, line): + """ + Called when a new line has been sent to the serial device + """ + + @abstractmethod + def on_line_received(self, line): + """ + Called when a new line has been received to the serial device + """ diff --git a/server/hw_controller/serial_device/firmwares/generic_firmware.py b/server/hw_controller/serial_device/firmwares/generic_firmware.py new file mode 100644 index 00000000..bbfe1206 --- /dev/null +++ b/server/hw_controller/serial_device/firmwares/generic_firmware.py @@ -0,0 +1,278 @@ +from collections import deque +import logging +import re + +from abc import ABC, abstractmethod +from threading import RLock, Lock +from py_expression_eval import Parser + +from server.hw_controller.serial_device.estimation.generic_estimator import GenericEstimator +from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.utils import buffered_timeout, limited_size_dict, settings_utils + +# Defines the character used to define macros +MACRO_CHAR = "&" + +# List of commands that are buffered by the controller +BUFFERED_COMMANDS = ("G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03", "G28", "G92") + + +class GenericFirmware(ABC): + """ + Abstract class for a firmware + + The implementer must handle the messages send to and received from the device + """ + + def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler): + """ + Args: + serial_settings: dict containing the serial settings + Must have: + serial_name: name of the serial port (like COM3 or tty/USB0) + baudrate: serial baudrate + logger: name of the logger to use for logging the communication + event_handler: event handler for line sent and line received + """ + + if serial_settings is None: + raise TypeError("The serial_device must not be None") + self._serial_settings = serial_settings + self._logger = logging.getLogger(logger) if not logger is None else logging.getLogger() + self.event_handler = event_handler + + self._serial_device = None + self._fast_mode = True # by default fast_mode is enabled + self.line_number = 0 + self._command_resolution = "{:.3f}" # by default will use 3 decimals in fast mode + self._mutex = RLock() + self.serial_mutex = Lock() + self._is_ready = False # the device will not be ready at the beginning + self.estimator = GenericEstimator() + + # buffer control + self.command_send_mutex = Lock() + self.command_buffer_mutex = Lock() + self.command_buffer = deque() + self.command_buffer_max_length = 8 + self.command_buffer_history = limited_size_dict.LimitedSizeDict( + size_limit=self.command_buffer_max_length + 40 + ) # keep saved the last n commands + self._buffered_line = "" + + # timeout setup + self.buffer_command = "" # command used to force an ack + # timeout used to clear the buffer if some acks are lost + self._timeout_last_line = 0 + self._timeout = buffered_timeout.BufferTimeout(30, self._on_timeout) + self._timeout.start() + + # regex generation for the macro parser + self.macro_regex = re.compile( + MACRO_CHAR + "(.*?)" + MACRO_CHAR + ) # looks for stuff between two "%" symbols. Used to parse macros + + self.macro_parser = Parser() # macro expressions parser + + @property + def fast_mode(self): + """ + Returns: + True if the device is using fast mode (GCODE is sent without blanks) + """ + with self._mutex: + return self._fast_mode + + @fast_mode.setter + def fast_mode(self, mode): + """ + Set fast mode + + Args: + mode: True if fast mode must be used (blank spaces are removed from the gcode line) + """ + with self._mutex: + self._fast_mode = mode + + @property + def feedrate(self): + """ + Returns the current feedrate + """ + with self._mutex: + return self._feedrate + + @feedrate.setter + def feedrate(self, feedrate): + """ + Set the current feedrate + """ + with self._mutex: + self._feedrate = feedrate + + def is_ready(self): + """ + Returns: + True if the device can be used + False if the device has not been initialized correctly yet + """ + with self._mutex: + return self._is_ready + + def get_current_position(self): + """ + Returns: + coords: current x and y position in a dict + """ + with self._mutex: + return self.estimator.position + + def _parse_macro(self, command): + """ + Parse a macro + + Macros are defined by the MACRO_CHAR. + The method substitute the formula in the macro with the correct value + """ + if not MACRO_CHAR in command: + return command + macros = self.macro_regex.findall(command) + for m in macros: + try: + # see https://pypi.org/project/py-expression-eval/ for more info about the parser + pos = self.estimator.get_last_commanded_position() + res = self.macro_parser.parse(m).evaluate( + {"X": pos.x, "Y": pos.y, "F": self.estimator.feedrate} + ) + command = command.replace(MACRO_CHAR + m + MACRO_CHAR, str(res)) + except Exception as e: + # TODO handle this in a better way + self._logger.error("Error while parsing macro: " + m) + self._logger.error(e) + return command + + def _prepare_command(self, command): + """ + Clean and prepare the current command to be sent to the device + + Args: + command: the gcode command to use + + Returns: + a string with the cleaned command + """ + with self._mutex: + command = command.replace("\n", "").replace("\r", "").upper() + + if command == " " or command == "": + return + + return self._parse_macro(command) + + def send_gcode_command(self, command, hide_command=False): + """ + Send the command + """ + with self._mutex: + command = self._prepare_command(command) + self.estimator.parse_command(command) + self._handle_send_command(command, hide_command) + self._update_timeout() # update the timeout because a new command has been sent + + def _handle_send_command(self, command, hide_command=False): + """ + Send the gcode command to the device and handle the buffer + """ + with self._mutex: + # wait until the lock for the buffer length is released + # if the lock is released means the board sent the ack for older lines and can send new ones + with self.command_send_mutex: + pass + + # send the command after parsing the content + # need to use the mutex here because it is changing also the line number + with self.serial_mutex: + line = self._generate_line(command) + + self._serial_device.send(line) # send line + self._logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) + + # TODO fix the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident. Do not understand why it is happening + + with self.command_buffer_mutex: + 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 + + if not hide_command: + self.event_handler.on_line_sent(line) # uses the handler callback for the new line + + def _generate_line(self, command, n=None): + """ + Handles the line before sending it + """ + with self._mutex: + line = command + # if is using fast mode need to reduce numbers resolution and remove spaces + + if self.fast_mode: + line = command.split(" ") + new_line = [] + for l in line: + if l.startswith("X"): + l = "X" + self._command_resolution.format(float(l[1:])).rstrip("0").rstrip( + "." + ) + elif l.startswith("Y"): + l = "Y" + self._command_resolution.format(float(l[1:])).rstrip("0").rstrip( + "." + ) + new_line.append(l) + line = "".join(new_line) + line += "\n" + + self.line_number += 1 + return line + + def _on_timeout(self): + """ + Callback for when the timeout is expired + """ + with self._mutex: + if self.command_buffer_mutex.locked and self.line_number == self._timeout_last_line: + # self.logger.warning("!Buffer timeout. Trying to clean the buffer!") + # to clean the buffer try to send a buffer update message. In this way will trigger the buffer cleaning mechanism + command = self.buffer_command + line = self._generate_line(command) + self._logger.log(settings_utils.LINE_SERVICE, line) + with self.serial_mutex: + self._serial_device.send(line) + else: + self._update_timeout() + + def _update_timeout(self): + """ + Update the timeout object in such a way that the interval is restored + """ + self._timeout_last_line = self.line_number + self._timeout.update() + + # From here on the methods are abstract and must be implemented in the child class + + @abstractmethod + def connect(self): + """ + Initialize the communication with the serial device + + Once the initializzation is done must set True the _is_ready flag + """ + pass + + @abstractmethod + def _on_readline(self, line): + """ + Parse a received line from the hw device + """ + pass diff --git a/server/hw_controller/serial_device/firmwares/marlin.py b/server/hw_controller/serial_device/firmwares/marlin.py new file mode 100644 index 00000000..41670d8a --- /dev/null +++ b/server/hw_controller/serial_device/firmwares/marlin.py @@ -0,0 +1,111 @@ +from threading import Thread +import time + +from server.hw_controller.serial_device.device_serial import DeviceSerial +from server.hw_controller.serial_device.firmwares.generic_firmware import GenericFirmware + + +class Marlin(GenericFirmware): + """ + Handle devices running Marlin + """ + + def __init__(self, serial_settings, logger): + super().__init__(serial_settings, logger) + self._logger.info("Marlin device") + self._command_resolution = "{:.1f}" + # command used to update the buffer status or get a free ack + self.buffer_command = "M114" + + def connect(self): + """ + Start the connection procedure with the serial device + """ + with self._mutex: + self._logger.info("Connecting to the serial device") + with self.serial_mutex: + self._serial_device = DeviceSerial( + self._serial_settings["serial_name"], + self._serial_settings["baudrate"], + self._logger.name, + ) + self._serial_device.set_onreadline_callback(self._on_readline) + self._serial_device.open() + # wait device ready + if self._serial_device.is_fake: + self._is_ready = True + else: + # runs a delay to wait the device to be ready + # TODO make this better: check messages from the device to understand when is ready + def delay(): + time.sleep(5) + self._on_device_ready() + + th = Thread(target=delay, daemon=True) + th.name = "waiting_device_ready" + th.start() + + def _on_readline(self, line): + """ + Parse the line received from the device + Args: + line: line to be parse, received from the device usually + """ + with self._mutex: + pass + + def _generate_line(self, command, n=None): + """ + Clean the command, substitute the macro values and add checksum + + Args: + command: command to be generated + n: line number to use + + Returns: + the generated line with the checksum + """ + line = super()._generate_line(command) + line = line.replace("\n", "") + + # check if the command contain a "reset line number" (M110) + if "M110" in command: + cs = command.split(" ") + for c in cs: + if c[0] == "N": + self.line_number = int(c[1:]) - 1 + self.command_buffer.clear() + + # add checksum + if n is None: + n = self.line_number + if self.fast_mode: + line = "N{}{}".format(n, line) + else: + line = "N{} {} ".format(n, line) + # calculate marlin checksum according to the wiki + cs = 0 + for i in line: + cs = cs ^ ord(i) + cs &= 0xFF + + line += f"*{cs}\n" # add checksum to the line + return line + + def _on_device_ready(self): + """ + Run some commands when the device is ready + """ + with self._mutex: + self._reset_line_number() + + def _reset_line_number(self, line_number=2): + """ + Send a gcode command to reset the line numbering on the device + + Args: + line_number (optional): the line number that should start counting from + """ + with self._mutex: + self._logger.info("Resetting line number") + self.send_gcode_command("M110 N{}".format(line_number)) diff --git a/server/hw_controller/gcode_rescalers.py b/server/hw_controller/serial_device/gcode_rescalers.py similarity index 100% rename from server/hw_controller/gcode_rescalers.py rename to server/hw_controller/serial_device/gcode_rescalers.py diff --git a/server/hw_controller/feeder.py b/server/hw_controller/serial_device/old/feeder.py similarity index 70% rename from server/hw_controller/feeder.py rename to server/hw_controller/serial_device/old/feeder.py index 1ed1e510..35bb317a 100644 --- a/server/hw_controller/feeder.py +++ b/server/hw_controller/serial_device/old/feeder.py @@ -12,9 +12,10 @@ from server.utils import limited_size_dict, buffered_timeout, settings_utils from server.utils.logging_utils import formatter, MultiprocessRotatingFileHandler -from server.hw_controller.device_serial import DeviceSerial -from server.hw_controller.gcode_rescalers import Fit -import server.hw_controller.firmware_defaults as firmware +from server.hw_controller.serial_device.feeder_event_handler import FeederEventHandler +from server.hw_controller.serial_device.device_serial import DeviceSerial +from server.hw_controller.serial_device.gcode_rescalers import Fit +import server.hw_controller.serial_device.firmware_defaults as firmware from server.database.playlist_elements import DrawingElement, TimeElement from server.database.generic_playlist_element import UNKNOWN_PROGRESS @@ -25,49 +26,29 @@ """ - -class FeederEventHandler(): - # called when the drawing is finished - def on_element_ended(self, element): - pass - - # called when a new drawing is started - def on_element_started(self, element): - pass - - # called when the feeder receives a message from the hw that must be sent to the frontend - def on_message_received(self, line): - pass - - # called when a new line is sent through serial (real or fake) - def on_new_line(self, line): - pass - - def on_device_ready(self): - pass - - - # List of commands that are buffered by the controller -BUFFERED_COMMANDS = ("G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03", "G28") +BUFFERED_COMMANDS = ("G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03", "G28") # Defines the character used to define macros -MACRO_CHAR = "&" +MACRO_CHAR = "&" + -class Feeder(): - def __init__(self, handler = None, **kargvs): +class Feeder: + def __init__(self, handler=None, **kargvs): # logger setup self.logger = logging.getLogger(__name__) - self.logger.handlers = [] # remove all handlers - self.logger.propagate = False # set it to False to avoid passing it to the parent logger + self.logger.handlers = [] # remove all handlers + self.logger.propagate = False # set it to False to avoid passing it to the parent logger # add custom logging levels logging.addLevelName(settings_utils.LINE_SENT, "LINE_SENT") logging.addLevelName(settings_utils.LINE_RECEIVED, "LINE_RECEIVED") logging.addLevelName(settings_utils.LINE_SERVICE, "LINE_SERVICE") - self.logger.setLevel(settings_utils.LINE_SERVICE) # set to logger lowest level + self.logger.setLevel(settings_utils.LINE_SERVICE) # set to logger lowest level # create file logging handler - file_handler = MultiprocessRotatingFileHandler("server/logs/feeder.log", maxBytes=200000, backupCount=5) + file_handler = MultiprocessRotatingFileHandler( + "server/logs/feeder.log", maxBytes=200000, backupCount=5 + ) file_handler.setLevel(settings_utils.LINE_SERVICE) file_handler.setFormatter(formatter) self.logger.addHandler(file_handler) @@ -88,7 +69,6 @@ def __init__(self, handler = None, **kargvs): settings_utils.print_level(level, __name__.split(".")[-1]) - # variables setup self._current_element = None @@ -100,27 +80,38 @@ def __init__(self, handler = None, **kargvs): self.status_mutex = Lock() if handler is None: self.handler = FeederEventHandler() - else: self.handler = handler - self.serial = DeviceSerial(logger_name = __name__) + else: + self.handler = handler + self.serial = DeviceSerial(logger_name=__name__) self.line_number = 0 self._timeout_last_line = self.line_number self.feedrate = 0 - self.last_commanded_position = DotMap({"x":0, "y":0}) + self.last_commanded_position = DotMap({"x": 0, "y": 0}) # commands parser - self.feed_regex = re.compile("[F]([0-9.-]+)($|\s)") # looks for a +/- float number after an F, until the first space or the end of the line - self.x_regex = re.compile("[X]([0-9.-]+)($|\s)") # looks for a +/- float number after an X, until the first space or the end of the line - self.y_regex = re.compile("[Y]([0-9.-]+)($|\s)") # looks for a +/- float number after an Y, until the first space or the end of the line - self.macro_regex = re.compile(MACRO_CHAR+"(.*?)"+MACRO_CHAR) # looks for stuff between two "%" symbols. Used to parse macros - - self.macro_parser = Parser() # macro expressions parser + self.feed_regex = re.compile( + "[F]([0-9.-]+)($|\s)" + ) # looks for a +/- float number after an F, until the first space or the end of the line + self.x_regex = re.compile( + "[X]([0-9.-]+)($|\s)" + ) # looks for a +/- float number after an X, until the first space or the end of the line + self.y_regex = re.compile( + "[Y]([0-9.-]+)($|\s)" + ) # looks for a +/- float number after an Y, until the first space or the end of the line + self.macro_regex = re.compile( + MACRO_CHAR + "(.*?)" + MACRO_CHAR + ) # looks for stuff between two "%" symbols. Used to parse macros + + self.macro_parser = Parser() # macro expressions parser # buffer controll 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 = limited_size_dict.LimitedSizeDict(size_limit = self.command_buffer_max_length+40) # keep saved the last n commands + self.command_buffer_history = limited_size_dict.LimitedSizeDict( + size_limit=self.command_buffer_max_length + 40 + ) # keep saved the last n commands self._buffered_line = "" self._timeout = buffered_timeout.BufferTimeout(30, self._on_timeout) @@ -129,7 +120,6 @@ def __init__(self, handler = None, **kargvs): # device specific options self.update_settings(settings_utils.load_settings()) - def update_settings(self, settings): self.settings = settings self._firmware = settings["device"]["firmware"]["value"] @@ -138,18 +128,23 @@ def update_settings(self, settings): self.is_fast_mode = settings["serial"]["fast_mode"]["value"] if self.is_fast_mode: if settings["device"]["type"]["value"] == "Cartesian": - self.command_resolution = "{:.1f}" # Cartesian do not need extra resolution because already using mm as units. (TODO maybe with inches can have problems? needs to check) - else: self.command_resolution = "{:.3f}" # Polar and scara use smaller numbers, will need also decimals - + self.command_resolution = "{:.1f}" # Cartesian do not need extra resolution because already using mm as units. (TODO maybe with inches can have problems? needs to check) + else: + self.command_resolution = ( + "{:.3f}" # Polar and scara use smaller numbers, will need also decimals + ) + def close(self): self.serial.close() def get_status(self): with self.status_mutex: return { - "is_running": self._is_running, - "progress": self._current_element.get_progress(self.feedrate) if not self._current_element is None else UNKNOWN_PROGRESS, - "is_paused": self._is_paused + "is_running": self._is_running, + "progress": self._current_element.get_progress(self.feedrate) + if not self._current_element is None + else UNKNOWN_PROGRESS, + "is_paused": self._is_paused, } def connect(self): @@ -158,32 +153,35 @@ def connect(self): if not self.serial is None: self.serial.close() try: - self.serial = DeviceSerial(self.settings['serial']['port']["value"], self.settings['serial']['baud']["value"], logger_name = __name__) + self.serial = DeviceSerial( + self.settings["serial"]["port"]["value"], + self.settings["serial"]["baud"]["value"], + logger_name=__name__, + ) self.serial.set_onreadline_callback(self.on_serial_read) self.serial.start_reading() self.logger.info("Connection successfull") except: self.logger.info("Error during device connection") self.logger.info(traceback.print_exc()) - self.serial = DeviceSerial(logger_name = __name__) + self.serial = DeviceSerial(logger_name=__name__) self.serial.set_onreadline_callback(self.on_serial_read) self.serial.start_reading() - self.device_ready = False # this line is set to true as soon as the board sends a message - + self.device_ready = False # this line is set to true as soon as the board sends a message def set_event_handler(self, handler): self.handler = handler # starts to send gcode to the machine def start_element(self, element, force_stop=False): - if((not force_stop) and self.is_running()): - return False # if a file is already being sent it will not start a new one + if (not force_stop) and self.is_running(): + return False # if a file is already being sent it will not start a new one else: if self.is_running(): - self.stop() # stop -> blocking function: wait until the thread is stopped for real + self.stop() # stop -> blocking function: wait until the thread is stopped for real with self.serial_mutex: - self._th = Thread(target = self._thf, args=(element,), daemon=True) + self._th = Thread(target=self._thf, args=(element,), daemon=True) self._th.name = "drawing_feeder" self._is_running = True self._stopped = False @@ -216,18 +214,18 @@ def update_current_time_element(self, new_interval): if type(self._current_element) is TimeElement: if self._current_element.type == "delay": self._current_element.update_delay(new_interval) - + # stops the drawing # blocking function: waits until the thread is stopped def stop(self): - if(self.is_running()): + if self.is_running(): tmp = self._current_element with self.status_mutex: if not self._stopped: self.logger.info("Stopping drawing") self._is_running = False self._current_element = None - # block the function until the thread is stopped otherwise the thread may still be running when the new thread is started + # block the function until the thread is stopped otherwise the thread may still be running when the new thread is started # (_isrunning will turn True and the old thread will keep going) while True: with self.status_mutex: @@ -236,8 +234,12 @@ def stop(self): # waiting command buffer to be clear before calling the "drawing ended" event while True: - self.send_gcode_command(firmware.get_buffer_command(self._firmware), hide_command=True) - time.sleep(3) # wait 3 second to get the time to the board to answer. If the time here is reduced too much will fill the buffer history with buffer_commands and may loose the needed line in a resend command for marlin + self.send_gcode_command( + firmware.get_buffer_command(self._firmware), hide_command=True + ) + time.sleep( + 3 + ) # wait 3 second to get the time to the board to answer. If the time here is reduced too much will fill the buffer history with buffer_commands and may loose the needed line in a resend command for marlin # the "buffer_command" will raise a response from the board that will be handled by the parser to empty the buffer # wait until the buffer is empty to know that the job is done @@ -248,15 +250,14 @@ def stop(self): self._reset_line_number() # calling "drawing ended" event self.handler.on_element_ended(tmp) - - + # pauses the drawing # can resume with "resume()" def pause(self): with self.status_mutex: self._is_paused = True self.logger.info("Paused") - + # resumes the drawing (only if used with "pause()" and not "stop()") def resume(self): with self.status_mutex: @@ -278,14 +279,14 @@ def send_gcode_command(self, command, hide_command=False): command = command.replace("\n", "").replace("\r", "").upper() if command == " " or command == "": return - + # some commands require to update the feeder status # parse the command if necessary if "M110" in command: cs = command.split(" ") for c in cs: - if c[0]=="N": - self.line_number = int(c[1:]) -1 + if c[0] == "N": + self.line_number = int(c[1:]) - 1 self.command_buffer.clear() # check if the command is in the "BUFFERED_COMMANDS" list and stops if the buffer is full @@ -300,7 +301,7 @@ def send_gcode_command(self, command, hide_command=False): except: self.logger.error("Cannot parse something in the command: " + command) # wait until the lock for the buffer length is released -> means the board sent the ack for older lines and can send new ones - with self.command_send_mutex: # wait until get some "ok" command to remove extra entries from the buffer + with self.command_send_mutex: # wait until get some "ok" command to remove extra entries from the buffer pass # send the command after parsing the content @@ -308,23 +309,26 @@ def send_gcode_command(self, command, hide_command=False): with self.serial_mutex: line = self._generate_line(command) - self.serial.send(line) # send line - self.logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) + self.serial.send(line) # send line + self.logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) # TODO fix the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident. Do not understand why it is happening with self.command_buffer_mutex: - 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 + 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 if not hide_command: - self.handler.on_new_line(line) # uses the handler callback for the new line - - if firmware.is_marlin(self._firmware): # updating the command only for marlin because grbl check periodically the buffer status with the status report command - self._update_timeout() # update the timeout because a new command has been sent + self.handler.on_new_line(line) # uses the handler callback for the new line + if firmware.is_marlin( + self._firmware + ): # updating the command only for marlin because grbl check periodically the buffer status with the status report command + self._update_timeout() # update the timeout because a new command has been sent - # Send a multiline script def send_script(self, script): self.logger.info("Sending script") @@ -338,7 +342,7 @@ def serial_ports_list(self): if not self.serial is None: result = self.serial.serial_port_list() return result - + def is_connected(self): with self.serial_mutex: return self.serial.is_connected() @@ -354,7 +358,7 @@ def emergency_stop(self): def _on_device_ready(self): if firmware.is_marlin(self._firmware): self._reset_line_number() - + # grbl status report mask setup # sandypi need to check the buffer to see if the machine has cleaned the buffer # setup grbl to show the buffer status with the $10 command @@ -365,9 +369,9 @@ def _on_device_ready(self): # the buffer will contain Bf:"usage of the buffer" if firmware.is_grbl(self._firmware): self.send_gcode_command("$10=6") - + # send the "on connection" script from the settings - self.send_script(self.settings['scripts']['connected']["value"]) + self.send_script(self.settings["scripts"]["connected"]["value"]) # device ready event self.handler.on_device_ready() @@ -377,7 +381,8 @@ def _on_device_ready_delay(self): def delay(): time.sleep(5) self._on_device_ready() - th = Thread(target = delay, daemon=True) + + th = Thread(target=delay, daemon=True) th.name = "waiting_device_ready" th.start() @@ -386,20 +391,31 @@ def delay(): def _thf(self, element): # runs the script only it the element is a drawing, otherwise will skip the "before" script if isinstance(element, DrawingElement): - self.send_script(self.settings['scripts']['before']["value"]) + self.send_script(self.settings["scripts"]["before"]["value"]) self.logger.info("Starting new drawing with code {}".format(element)) - + # 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} - + dims = { + "table_x": 100, + "table_y": 100, + "drawing_max_x": 100, + "drawing_max_y": 100, + "drawing_min_x": 0, + "drawing_min_y": 0, + } + filter = Fit(dims) - - for k, line in enumerate(self.get_element().execute(self.logger)): # execute the element (iterate over the commands or do what the element is designed for) + + for k, line in enumerate( + self.get_element().execute(self.logger) + ): # execute the element (iterate over the commands or do what the element is designed for) if not self.is_running(): break - - if line is None: # if the line is none there is no command to send, will continue with the next element execution (for example, within the delay element it will sleep 1s at a time and return None until the timeout passed. TODO Not really an efficient way, may change it in the future) + + if ( + line is None + ): # if the line is none there is no command to send, will continue with the next element execution (for example, within the delay element it will sleep 1s at a time and return None until the timeout passed. TODO Not really an efficient way, may change it in the future) continue line = line.upper() @@ -413,45 +429,51 @@ def _thf(self, element): break # TODO parse line to scale/add padding to the drawing according to the drawing settings (in order to keep the original .gcode file) - #line = filter.parse_line(line) - #line = "N{} ".format(file_line) + line + # line = filter.parse_line(line) + # line = "N{} ".format(file_line) + line with self.status_mutex: self._stopped = True - + # runs the script only it the element is a drawing, otherwise will skip the "after" script if isinstance(element, DrawingElement): - self.send_script(self.settings['scripts']['after']["value"]) + self.send_script(self.settings["scripts"]["after"]["value"]) if self.is_running(): self.stop() # thread that keep reading the serial port def on_serial_read(self, l): if not l is None: - # readline is not returning the full line but only a buffer + # readline is not returning the full line but only a buffer # must break the line on "\n" to correctly parse the result self._buffered_line += l if "\n" in self._buffered_line: self._buffered_line = self._buffered_line.replace("\r", "").split("\n") - if len(self._buffered_line) >1: - for l in self._buffered_line[0:-1]: # parse single lines if multiple \n are detected + if len(self._buffered_line) > 1: + for l in self._buffered_line[ + 0:-1 + ]: # parse single lines if multiple \n are detected self._parse_device_line(l) self._buffered_line = str(self._buffered_line[-1]) - def _update_timeout(self): self._timeout_last_line = self.line_number self._timeout.update() - # function called when the buffer has not been updated for some time (controlled by the buffered timeou) def _on_timeout(self): - if (self.command_buffer_mutex.locked and self.line_number == self._timeout_last_line and not self.is_paused()): + if ( + self.command_buffer_mutex.locked + and self.line_number == self._timeout_last_line + and not self.is_paused() + ): # self.logger.warning("!Buffer timeout. Trying to clean the buffer!") # to clean the buffer try to send an M114 (marlin) or ? (Grbl) message. In this way will trigger the buffer cleaning mechanism command = firmware.get_buffer_command(self._firmware) - line = self._generate_line(command, no_buffer=True) # use the no_buffer to clean one position of the buffer after adding the command + line = self._generate_line( + command, no_buffer=True + ) # use the no_buffer to clean one position of the buffer after adding the command self.logger.log(settings_utils.LINE_SERVICE, line) - with self.serial_mutex: + with self.serial_mutex: self.serial.send(line) else: self._update_timeout() @@ -462,7 +484,7 @@ def _ack_received(self, safe_line_number=None, append_left_extra=False): if len(self.command_buffer) != 0: self.command_buffer.popleft() else: - with self.command_buffer_mutex: + with self.command_buffer_mutex: while True: # Remove the numbers lower than the specified safe_line_number (used in the resend line command: lines older than the one required can be deleted safely) if len(self.command_buffer) != 0: @@ -471,17 +493,18 @@ def _ack_received(self, safe_line_number=None, append_left_extra=False): self.command_buffer.appendleft(line_number) break if append_left_extra: - self.command_buffer.appendleft(safe_line_number-1) + self.command_buffer.appendleft(safe_line_number - 1) self._check_buffer_mutex_status() - # check if the buffer of the device is full or can accept more commands def _check_buffer_mutex_status(self): with self.command_buffer_mutex: - if self.command_send_mutex.locked() and len(self.command_buffer) < self.command_buffer_max_length: + if ( + self.command_send_mutex.locked() + and len(self.command_buffer) < self.command_buffer_max_length + ): self.command_send_mutex.release() - # parse a line coming from the device def _parse_device_line(self, line): @@ -489,37 +512,41 @@ def _parse_device_line(self, line): # will still print the status in the command line hide_line = False - if firmware.get_ACK(self._firmware) in line: # when an "ack" is received free one place in the buffer + if ( + firmware.get_ACK(self._firmware) in line + ): # when an "ack" is received free one place in the buffer self._ack_received() - + # check if the received line is for the device being ready if firmware.get_ready_message(self._firmware) in line: if self.serial.is_fake: self._on_device_ready() else: - self._on_device_ready_delay() # if the device is ready will allow the communication after a small delay + self._on_device_ready_delay() # if the device is ready will allow the communication after a small delay # check marlin specific messages if firmware.is_grbl(self._firmware): if line.startswith("<"): try: # interested in the "Bf:xx," part where xx is the content of the buffer - # select buffer content lines + # select buffer content lines res = line.split("Bf:")[1] res = int(res.split(",")[0]) - if res == 15: # 15 => buffer is empty on the device (should include also 14 to make it more flexible?) + if ( + res == 15 + ): # 15 => buffer is empty on the device (should include also 14 to make it more flexible?) with self.command_buffer_mutex: self.command_buffer.clear() - if res!= 0: # 0 -> buffer is full + if res != 0: # 0 -> buffer is full with self.command_buffer_mutex: if len(self.command_buffer) > 0 and self.is_running(): self.command_buffer.popleft() self._check_buffer_mutex_status() - - if (self.is_running() or self.is_paused()): + + if self.is_running() or self.is_paused(): hide_line = True self.logger.log(settings_utils.LINE_SERVICE, line) - except: # sometimes may not receive the entire line thus it may throw an error + except: # sometimes may not receive the entire line thus it may throw an error pass return @@ -532,7 +559,6 @@ def _parse_device_line(self, line): self.logger.error("Grbl error: {}".format(line)) # TODO check/parse error types and give some hint about the problem? - # TODO divide parser between firmwares? # TODO set firmware type automatically on connection # TODO add feedrate control with something like a knob on the user interface to make the drawing slower or faster @@ -558,16 +584,22 @@ def _parse_device_line(self, line): first_available_line = line_number # All the lines after the required one must be resent. Cannot break the loop now self.serial.send(c) - self.logger.error("Line not received correctly. Resending: {}".format(c.strip("\n"))) + self.logger.error( + "Line not received correctly. Resending: {}".format(c.strip("\n")) + ) - if (not line_found) and not(first_available_line is None): + if (not line_found) and not (first_available_line is None): for i in range(line_number, first_available_line): - self.serial.send(self._generate_line(firmware.MARLIN.buffer_command, no_buffer=True, n=i)) + self.serial.send( + self._generate_line(firmware.MARLIN.buffer_command, no_buffer=True, n=i) + ) - self._ack_received(safe_line_number=line_number-1, append_left_extra=True) + 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: - self.logger.error("No line was found for the number required. Restart numeration.") + if not line_found: + self.logger.error( + "No line was found for the number required. Restart numeration." + ) self._reset_line_number() # Marlin "unknow command" @@ -579,15 +611,21 @@ def _parse_device_line(self, line): elif "Count" in line: try: l = line.split(" ") - x = float(l[0][2:]) # remove "X:" from the string - y = float(l[1][2:]) # remove "Y:" from the string + x = float(l[0][2:]) # remove "X:" from the string + y = float(l[1][2:]) # remove "Y:" from the string except Exception as e: self.logger.error("Error while parsing M114 result for line: {}".format(line)) self.logger.exception(e) # if the last commanded position coincides with the current position it means the buffer on the device is empty (could happen that the position is the same between different points but the M114 command should not be that frequent to run into this problem.) TODO check if it is good enough or if should implement additional checks like a timeout # use a tolerance instead of equality because marlin is using strange rounding for the coordinates - if (abs(float(self.last_commanded_position.x)-x) Date: Sun, 30 Jan 2022 13:52:48 +0100 Subject: [PATCH 07/52] Adding readline and buffer control Tried with emulator. Need to test it with a real board --- server/__init__.py | 7 +- .../serial_device/device_serial.py | 104 +++++++++++------ .../hw_controller/serial_device/emulator.py | 28 +++-- .../estimation/generic_estimator.py | 3 + server/hw_controller/serial_device/feeder.py | 3 + .../firmwares/commands_buffer.py | 96 ++++++++++++++++ .../firmwares/generic_firmware.py | 87 +++++++++------ .../serial_device/firmwares/marlin.py | 105 ++++++++++++++++-- test.py | 33 ++++++ 9 files changed, 379 insertions(+), 87 deletions(-) create mode 100644 server/hw_controller/serial_device/firmwares/commands_buffer.py create mode 100644 test.py diff --git a/server/__init__.py b/server/__init__.py index 1347bc3c..53c26d41 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -99,9 +99,8 @@ def base_static(filename): import server.api.drawings from server.sockets_interface.socketio_emits import SocketioEmits import server.sockets_interface.socketio_callbacks + from server.hw_controller.serial_device.feeder import Feeder from server.hw_controller.queue_manager import QueueManager - from server.hw_controller.feeder import Feeder - from server.hw_controller.feeder_event_manager import FeederEventManager from server.preprocessing.file_observer import GcodeObserverManager from server.hw_controller.leds.leds_controller import LedsController from server.hw_controller.buttons.buttons_manager import ButtonsManager @@ -114,7 +113,7 @@ def base_static(filename): app.semits = SocketioEmits(app, socketio, db) # Device controller initialization -app.feeder = Feeder(FeederEventManager(app)) +app.feeder = Feeder() app.qmanager = QueueManager(app, socketio) # Buttons controller initialization @@ -152,7 +151,7 @@ def home(): # Starting the feeder after the server is ready to avoid problems with the web page not showing up def run_post(): sleep(2) - app.feeder.connect() + # app.feeder.connect() app.lmanager.start() diff --git a/server/hw_controller/serial_device/device_serial.py b/server/hw_controller/serial_device/device_serial.py index d7bbee0f..21cf46cf 100644 --- a/server/hw_controller/serial_device/device_serial.py +++ b/server/hw_controller/serial_device/device_serial.py @@ -1,4 +1,4 @@ -from threading import Thread, Lock +from threading import Thread, RLock import serial.tools.list_ports import serial import sys @@ -7,12 +7,20 @@ from server.hw_controller.serial_device.emulator import Emulator -# This class connects to a serial device -# If the serial device request is not available it will create a virtual serial device - class DeviceSerial: + """ + Connect to a serial device + If the serial device request is not available it will create a virtual serial device + """ + def __init__(self, serial_name=None, baudrate=115200, logger_name=None): + """ + Args: + serial_name: name of the serial port to use + baudrate: baudrate value to use + logger_name: name of the logger to use + """ self.logger = ( logging.getLogger(logger_name) if not logger_name is None else logging.getLogger() ) @@ -29,13 +37,17 @@ def useless(arg): # setting up the read thread self._th = Thread(target=self._thf, daemon=True) - self._mutex = Lock() + self._mutex = RLock() self._th.name = "serial_read" self._running = False - self.set_onreadline_callback(useless) + self.set_on_readline_callback(useless) def open(self): - # opening serial + """ + Open the serial port + + If the port is not working, work as a virtual device + """ try: args = dict(baudrate=self.baudrate, timeout=0, write_timeout=0) self.serial = serial.Serial(**args) @@ -52,22 +64,39 @@ def open(self): self._th.start() - # this method is used to set a callback for the "new line available" event - def set_onreadline_callback(self, callback): + def set_on_readline_callback(self, callback): + """ + Set the a callback for a new line received + + Args: + callback: the function to call when a new line is received. The function will receive the line as an argument + """ self._on_readline = callback - # check if the reading thread is working def is_running(self): + """ + Check if the reading thread is running + + Returns: + True if the readline thread is running + """ return self._running - # stops the serial read thread def stop(self): + """ + Stop the serial read thread + """ self._running = False - # sends a line to the device - def send(self, obj): + def send(self, line): + """ + Send a line to the device + + Args: + line: the line to send to the device + """ if self.is_fake: - self._emulator.send(obj) + self._emulator.send(line) else: if self.serial.is_open: try: @@ -75,32 +104,25 @@ def send(self, obj): while self.serial.out_waiting: pass # TODO should add a sort of timeout self._readline() - self.serial.write(str(obj).encode()) + self.serial.write(str(line).encode()) # TODO try to send byte by byte instead of a full line? (to reduce the risk of sending commands with missing digits or wrong values that may lead to a wrong position value) except: self.close() self.logger.error("Error while sending a command") - # return a list of available serial ports - 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 - - # check if is connected to a real device def is_connected(self): + """ + Returns: + True if the serial is open on a real device + """ if self.is_fake: return False return self.serial.is_open - # close the connection with the serial device def close(self): + """ + Close the connection with the serial device + """ self.stop() try: self.serial.close() @@ -110,8 +132,10 @@ def close(self): # private functions - # reads a line from the device def _readline(self): + """ + Reads a line from the device (if available) and call the callback + """ if not self.is_fake: if self.serial.is_open: while self.serial.in_waiting > 0: @@ -120,8 +144,10 @@ def _readline(self): else: return self._emulator.readline() - # thread function def _thf(self): + """ + Thread function for the readline + """ self._running = True next_line = "" while self.is_running(): @@ -129,3 +155,19 @@ def _thf(self): next_line = self._readline() # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method self._on_readline(next_line) + + @classmethod + def get_serial_port_list(cls): + """ + Returns: + list of the names of the available serial ports + """ + 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 diff --git a/server/hw_controller/serial_device/emulator.py b/server/hw_controller/serial_device/emulator.py index c0bf8616..4bc6f8d1 100644 --- a/server/hw_controller/serial_device/emulator.py +++ b/server/hw_controller/serial_device/emulator.py @@ -2,17 +2,17 @@ from collections import deque from server.utils.settings_utils import load_settings -import server.hw_controller.firmware_defaults as firmware emulated_commands_with_delay = ["G0", "G00", "G1", "G01"] ACK = "ok\n\r" -class Emulator(): + +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.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)") @@ -20,20 +20,21 @@ def __init__(self): self.last_x = 0.0 self.last_y = 0.0 self.settings = load_settings() - self.message_buffer.append(firmware.get_ready_message(self.settings["device"]["firmware"]["value"])+"\n") # sends back a message to tell the board is ready and can receive commands 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 + 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 @@ -60,8 +61,11 @@ def send(self, command): y = self.last_y # calculate time self.feedrate = max(self.feedrate, 0.01) - t = max(math.sqrt((x-self.last_x)**2 + (y-self.last_y)**2) / self.feedrate * 60.0, 0.1) # TODO need to use the max 0.005 because cannot simulate anything on the frontend otherwise... May look for a better solution - + t = max( + math.sqrt((x - self.last_x) ** 2 + (y - self.last_y) ** 2) / self.feedrate * 60.0, + 0.1, + ) # TODO need to use the max 0.005 because cannot simulate anything on the frontend otherwise... May look for a better solution + # update positions self.last_x = x self.last_y = y @@ -77,7 +81,7 @@ 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 @@ -86,4 +90,4 @@ def readline(self): self.ack_buffer.appendleft(oldest) return None else: - return ACK \ No newline at end of file + return ACK diff --git a/server/hw_controller/serial_device/estimation/generic_estimator.py b/server/hw_controller/serial_device/estimation/generic_estimator.py index d9839a06..59e43c38 100644 --- a/server/hw_controller/serial_device/estimation/generic_estimator.py +++ b/server/hw_controller/serial_device/estimation/generic_estimator.py @@ -62,6 +62,9 @@ def get_last_commanded_position(self): return self._position def reset_path_length(self): + """ + Reset traveled path length + """ self._path_length = 0 def parse_command(self, command): diff --git a/server/hw_controller/serial_device/feeder.py b/server/hw_controller/serial_device/feeder.py index 54ade125..8efdb99a 100644 --- a/server/hw_controller/serial_device/feeder.py +++ b/server/hw_controller/serial_device/feeder.py @@ -1,3 +1,6 @@ class Feeder: def __init__(self): pass + + def get_status(self): + return {"is_running": True} diff --git a/server/hw_controller/serial_device/firmwares/commands_buffer.py b/server/hw_controller/serial_device/firmwares/commands_buffer.py new file mode 100644 index 00000000..92ff4cd1 --- /dev/null +++ b/server/hw_controller/serial_device/firmwares/commands_buffer.py @@ -0,0 +1,96 @@ +from collections import deque +from threading import RLock, Lock + +from server.utils import limited_size_dict + + +class CommandBuffer: + def __init__(self, max_length=8): + """ + Args: + max_length (int): the maximum number of messages that can be sent in a row without receiving acks + """ + # this lock is used to check if the buffer is full + # if the mutex is locker means that must wait before sending a new line + self._send_mutex = Lock() + # this lock is just to access the buffer + self._mutex = RLock() + # command buffers + self._buffer = deque() + # max number of messages + self._buffer_max_length = max_length + # keep saved the last n commands (this will be helpfull for marlin) + self._buffer_history = limited_size_dict.LimitedSizeDict( + size_limit=self._buffer_max_length + 40 + ) + + def is_empty(self): + """ + Returns: + True if the buffer is empty + """ + return len(self._buffer) > 0 + + def get_buffer_wait_mutex(self): + """ + Returns: + send_mutex: the mutex is locked if the buffer is full and cannot send more commands + """ + return self._send_mutex + + def push_command(self, command, line_number, no_buffer=False): + """ + Args: + command: the command that must be buffered + line_number: the line number to be buffered + no_buffer: if False, will not make the buffer longer (used with the control commands) + """ + with self._mutex: + self._buffer.append(line_number) + self._buffer_history[f"N{line_number}"] = command + if no_buffer: + self._buffer.popleft() # remove an element to get a free ack from the non buffered command. Still must keep it in the buffer in the case of an error in sending the line + + if len(self._buffer) >= self._buffer_max_length and not self._send_mutex.locked(): + self._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 + + def clear(self): + """ + Clear the buffer + """ + with self._mutex: + self._buffer.clear() + + def ack_received(self, safe_line_number=None, append_left_extra=False): + """ + This method must be called when a new ack has been received + Clear the oldest sent command in order to free up space in the buffer + + Args: + safe_line_number: if set, will delete all the commands older than the given line number + append_left_extra: adds extra entry (should be used together with safe_line_number) + """ + with self._mutex: + if safe_line_number is None: + if len(self._buffer) != 0: + self._buffer.popleft() + else: + while True: + # Remove the numbers lower than the specified safe_line_number (used in the resend line command: lines older than the one required can be deleted safely) + if len(self._buffer) != 0: + line_number = self._buffer.popleft() + if line_number >= safe_line_number: + self._buffer.appendleft(line_number) + break + if append_left_extra: + self._buffer.appendleft(safe_line_number - 1) + + self.check_buffer_mutex_status() + + def check_buffer_mutex_status(self): + """ + Check if the send lock must be released + """ + with self._mutex: + if self._send_mutex.locked() and len(self._buffer) < self._buffer_max_length: + self._send_mutex.release() diff --git a/server/hw_controller/serial_device/firmwares/generic_firmware.py b/server/hw_controller/serial_device/firmwares/generic_firmware.py index bbfe1206..86f1e7c1 100644 --- a/server/hw_controller/serial_device/firmwares/generic_firmware.py +++ b/server/hw_controller/serial_device/firmwares/generic_firmware.py @@ -7,8 +7,9 @@ from py_expression_eval import Parser from server.hw_controller.serial_device.estimation.generic_estimator import GenericEstimator +from server.hw_controller.serial_device.firmwares.commands_buffer import CommandBuffer from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler -from server.utils import buffered_timeout, limited_size_dict, settings_utils +from server.utils import buffered_timeout, settings_utils # Defines the character used to define macros MACRO_CHAR = "&" @@ -51,17 +52,11 @@ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler) self.estimator = GenericEstimator() # buffer control - self.command_send_mutex = Lock() - self.command_buffer_mutex = Lock() - self.command_buffer = deque() - self.command_buffer_max_length = 8 - self.command_buffer_history = limited_size_dict.LimitedSizeDict( - size_limit=self.command_buffer_max_length + 40 - ) # keep saved the last n commands - self._buffered_line = "" + self.buffer = CommandBuffer(4) # timeout setup - self.buffer_command = "" # command used to force an ack + self.force_ack_command = "" # command used to force an ack + self.ack = "" # the ack string sent from the device # timeout used to clear the buffer if some acks are lost self._timeout_last_line = 0 self._timeout = buffered_timeout.BufferTimeout(30, self._on_timeout) @@ -119,6 +114,13 @@ def is_ready(self): with self._mutex: return self._is_ready + def is_connected(self): + """ + Returns: + True if the device is connected + """ + return self._serial_device.is_connected() + def get_current_position(self): """ Returns: @@ -173,21 +175,20 @@ def send_gcode_command(self, command, hide_command=False): """ Send the command """ - with self._mutex: - command = self._prepare_command(command) - self.estimator.parse_command(command) - self._handle_send_command(command, hide_command) - self._update_timeout() # update the timeout because a new command has been sent + command = self._prepare_command(command) + self.estimator.parse_command(command) + self._handle_send_command(command, hide_command) + self._update_timeout() # update the timeout because a new command has been sent def _handle_send_command(self, command, hide_command=False): """ Send the gcode command to the device and handle the buffer """ + # wait until the lock for the buffer length is released + # if the lock is released means the board sent the ack for older lines and can send new ones + with self.buffer.get_buffer_wait_mutex(): + pass with self._mutex: - # wait until the lock for the buffer length is released - # if the lock is released means the board sent the ack for older lines and can send new ones - with self.command_send_mutex: - pass # send the command after parsing the content # need to use the mutex here because it is changing also the line number @@ -195,17 +196,11 @@ def _handle_send_command(self, command, hide_command=False): line = self._generate_line(command) self._serial_device.send(line) # send line + self.buffer.push_command(line, self.line_number) self._logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) # TODO fix the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident. Do not understand why it is happening - with self.command_buffer_mutex: - 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 - if not hide_command: self.event_handler.on_line_sent(line) # uses the handler callback for the new line @@ -241,10 +236,13 @@ def _on_timeout(self): Callback for when the timeout is expired """ with self._mutex: - if self.command_buffer_mutex.locked and self.line_number == self._timeout_last_line: + if ( + self.buffer.get_buffer_wait_mutex().locked + and self.line_number == self._timeout_last_line + ): # self.logger.warning("!Buffer timeout. Trying to clean the buffer!") # to clean the buffer try to send a buffer update message. In this way will trigger the buffer cleaning mechanism - command = self.buffer_command + command = self.force_ack_command line = self._generate_line(command) self._logger.log(settings_utils.LINE_SERVICE, line) with self.serial_mutex: @@ -259,6 +257,33 @@ def _update_timeout(self): self._timeout_last_line = self.line_number self._timeout.update() + def _on_readline(self, line): + """ + Parse a received line from the hw device + + Returns: + True if the readline has done correctly + """ + with self._mutex: + if line is None: + return False + if self.ack in line: + self.buffer.ack_received() + return True + + def _log_received_line(self, line, hide_line=False): + """ + Log the line received from the device + Not called automatically in the _on_readline + + Args: + line: the line received from the device + hide_line: if True will not send the line to the frontend + """ + self._logger.log(settings_utils.LINE_RECEIVED, line) + if not hide_line: + self.event_handler.on_line_received(line) + # From here on the methods are abstract and must be implemented in the child class @abstractmethod @@ -268,11 +293,9 @@ def connect(self): Once the initializzation is done must set True the _is_ready flag """ - pass @abstractmethod - def _on_readline(self, line): + def emergency_stop(self): """ - Parse a received line from the hw device + Stop the device immediately """ - pass diff --git a/server/hw_controller/serial_device/firmwares/marlin.py b/server/hw_controller/serial_device/firmwares/marlin.py index 41670d8a..66dbd4e8 100644 --- a/server/hw_controller/serial_device/firmwares/marlin.py +++ b/server/hw_controller/serial_device/firmwares/marlin.py @@ -1,7 +1,9 @@ +from copy import deepcopy from threading import Thread import time from server.hw_controller.serial_device.device_serial import DeviceSerial +from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler from server.hw_controller.serial_device.firmwares.generic_firmware import GenericFirmware @@ -10,12 +12,17 @@ class Marlin(GenericFirmware): Handle devices running Marlin """ - def __init__(self, serial_settings, logger): - super().__init__(serial_settings, logger) + def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler): + super().__init__(serial_settings, logger, event_handler) self._logger.info("Marlin device") + # marlin specific values self._command_resolution = "{:.1f}" # command used to update the buffer status or get a free ack - self.buffer_command = "M114" + self.force_ack_command = "M114" + # ack string from the device + self.ack = "ok" + # tolerance position (needed because the marlin rounding for the actual position is not the usual rounding) + self.position_tolerance = 0.01 def connect(self): """ @@ -29,7 +36,7 @@ def connect(self): self._serial_settings["baudrate"], self._logger.name, ) - self._serial_device.set_onreadline_callback(self._on_readline) + self._serial_device.set_on_readline_callback(self._on_readline) self._serial_device.open() # wait device ready if self._serial_device.is_fake: @@ -45,14 +52,96 @@ def delay(): th.name = "waiting_device_ready" th.start() + def emergency_stop(self): + """ + Stop the device immediately + """ + self.send_gcode_command("M112") + def _on_readline(self, line): """ Parse the line received from the device - Args: - line: line to be parse, received from the device usually + + Args: + line: line to be parse, received from the device usually + + Returns: + True if the line is handled correctly """ with self._mutex: - pass + # if the line is not valid will return False + if not super()._on_readline(line): + return False + + hide_line = False + # Resend + if "Resend: " in line: + line_found = False + line_number = int(line.replace("Resend: ", "").replace("\r\n", "")) + items = deepcopy(self.buffer._buffer_history) + first_available_line = None + for command_n, command in items.items(): + n_line_number = int(command_n.strip("N")) + if n_line_number == line_number: + line_found = True + if n_line_number >= line_number: + if first_available_line is None: + first_available_line = line_number + # All the lines after the required one must be resent. Cannot break the loop now + self._serial_device.send(command) + self._logger.error( + "Line not received correctly. Resending: {}".format(command.strip("\n")) + ) + + if (not line_found) and not (first_available_line is None): + for i in range(line_number, first_available_line): + self._serial_device.send(self._generate_line(self.force_ack_command, n=i)) + + self.buffer.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: + self._logger.error( + "No line was found for the number required. Restart numeration." + ) + self._reset_line_number() + + # unknow command + elif "echo:Unknown command:" in line: + self._logger.error("Error: command not found. Can also be a communication error") + + # M114 response contains the "Count" word + # the response looks like: X:115.22 Y:116.38 Z:0.00 E:0.00 Count A:9218 B:9310 Z:0 + # still, M114 will receive the last position in the look-ahead planner thus the drawing will end first on the interface and then in the real device + elif "Count" in line: + try: + l = line.split(" ") + x = float(l[0][2:]) # remove "X:" from the string + y = float(l[1][2:]) # remove "Y:" from the string + except Exception as e: + self._logger.error("Error while parsing M114 result for line: {}".format(line)) + self._logger.exception(e) + + commanded_position = self.estimator.get_last_commanded_position() + # if the last commanded position coincides with the current position it means the buffer on the device is empty (could happen that the position is the same between different points but the M114 command should not be that frequent to run into this problem.) TODO check if it is good enough or if should implement additional checks like a timeout + # use a tolerance instead of equality because marlin is using strange rounding for the coordinates + if (abs(float(commanded_position.x) - x) < self.position_tolerance) and ( + abs(float(commanded_position.y) - y) < self.position_tolerance + ): + if not self.buffer.is_empty(): + self.buffer.ack_received() + else: + self.buffer.clear() + self.buffer.check_buffer_mutex_status() + + if not self.buffer.is_empty(): + hide_line = True + + # 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 + + self._log_received_line(line, hide_line) + return True def _generate_line(self, command, n=None): """ @@ -74,7 +163,7 @@ def _generate_line(self, command, n=None): for c in cs: if c[0] == "N": self.line_number = int(c[1:]) - 1 - self.command_buffer.clear() + self.buffer.clear() # add checksum if n is None: diff --git a/test.py b/test.py new file mode 100644 index 00000000..9e9d84f7 --- /dev/null +++ b/test.py @@ -0,0 +1,33 @@ +# FIXME remove this + +import time +from server.hw_controller.serial_device.firmwares.marlin import Marlin +from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler +import logging + +if __name__ == "__main__": + + class EventHandler(FirwmareEventHandler): + def on_line_received(self, line): + print(f"Line received: {line}") + + def on_line_sent(self, line): + print(f"Line sent: {line}") + + device = Marlin( + serial_settings={"serial_name": "COM3", "baudrate": 115200}, + logger=logging.getLogger().name, + event_handler=EventHandler(), + ) + device.connect() + device.send_gcode_command("G28") + device.send_gcode_command("G0 X0 Y0 F300") + for x in range(15): + device.send_gcode_command(f"G0 X{x}00 Y0") + time.sleep(0.01) + time.sleep(10) + + print("Done") + + while True: + pass From f58e0a5edc63e466f4ecc02438e5bccb013cf76b Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Tue, 1 Feb 2022 22:37:02 +0100 Subject: [PATCH 08/52] Added grbl firmware class and fixed some problems with mutexes --- .../hw_controller/serial_device/emulator.py | 6 +- .../firmwares/commands_buffer.py | 7 ++ .../firmwares/firmware_event_handler.py | 6 ++ .../firmwares/generic_firmware.py | 71 +++++++++------ .../serial_device/firmwares/grbl.py | 91 +++++++++++++++++++ .../serial_device/firmwares/marlin.py | 38 ++------ test.py | 45 ++++++--- 7 files changed, 188 insertions(+), 76 deletions(-) create mode 100644 server/hw_controller/serial_device/firmwares/grbl.py diff --git a/server/hw_controller/serial_device/emulator.py b/server/hw_controller/serial_device/emulator.py index 4bc6f8d1..426a27b9 100644 --- a/server/hw_controller/serial_device/emulator.py +++ b/server/hw_controller/serial_device/emulator.py @@ -14,9 +14,9 @@ def __init__(self): 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.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 self.settings = load_settings() diff --git a/server/hw_controller/serial_device/firmwares/commands_buffer.py b/server/hw_controller/serial_device/firmwares/commands_buffer.py index 92ff4cd1..57ccf849 100644 --- a/server/hw_controller/serial_device/firmwares/commands_buffer.py +++ b/server/hw_controller/serial_device/firmwares/commands_buffer.py @@ -94,3 +94,10 @@ def check_buffer_mutex_status(self): with self._mutex: if self._send_mutex.locked() and len(self._buffer) < self._buffer_max_length: self._send_mutex.release() + + def popleft(self): + return self._buffer.popleft() + + def __len__(self): + with self._mutex: + return len(self._buffer) diff --git a/server/hw_controller/serial_device/firmwares/firmware_event_handler.py b/server/hw_controller/serial_device/firmwares/firmware_event_handler.py index 480a4622..f1798181 100644 --- a/server/hw_controller/serial_device/firmwares/firmware_event_handler.py +++ b/server/hw_controller/serial_device/firmwares/firmware_event_handler.py @@ -17,3 +17,9 @@ def on_line_received(self, line): """ Called when a new line has been received to the serial device """ + + @abstractmethod + def on_device_ready(self): + """ + Called when the connected device is ready + """ diff --git a/server/hw_controller/serial_device/firmwares/generic_firmware.py b/server/hw_controller/serial_device/firmwares/generic_firmware.py index 86f1e7c1..94fe1507 100644 --- a/server/hw_controller/serial_device/firmwares/generic_firmware.py +++ b/server/hw_controller/serial_device/firmwares/generic_firmware.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from threading import RLock, Lock from py_expression_eval import Parser +from server.hw_controller.serial_device.device_serial import DeviceSerial from server.hw_controller.serial_device.estimation.generic_estimator import GenericEstimator from server.hw_controller.serial_device.firmwares.commands_buffer import CommandBuffer @@ -52,11 +53,11 @@ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler) self.estimator = GenericEstimator() # buffer control - self.buffer = CommandBuffer(4) + self.buffer = CommandBuffer(8) # timeout setup self.force_ack_command = "" # command used to force an ack - self.ack = "" # the ack string sent from the device + self.ack = "ok" # the ack string sent from the device # timeout used to clear the buffer if some acks are lost self._timeout_last_line = 0 self._timeout = buffered_timeout.BufferTimeout(30, self._on_timeout) @@ -129,6 +130,38 @@ def get_current_position(self): with self._mutex: return self.estimator.position + def send_gcode_command(self, command, hide_command=False): + """ + Send the command + """ + command = self._prepare_command(command) + # wait until the lock for the buffer length is released + # if the lock is released means the board sent the ack for older lines and can send new ones + with self.buffer.get_buffer_wait_mutex(): + pass + with self._mutex: + self._handle_send_command(command, hide_command) + self.estimator.parse_command(command) + self._update_timeout() # update the timeout because a new command has been sent + + def connect(self): + """ + Start the connection procedure with the serial device + """ + with self._mutex: + self._logger.info("Connecting to the serial device") + with self.serial_mutex: + self._serial_device = DeviceSerial( + self._serial_settings["serial_name"], + self._serial_settings["baudrate"], + self._logger.name, + ) + self._serial_device.set_on_readline_callback(self._on_readline) + self._serial_device.open() + # wait device ready + if not self._serial_device.is_connected(): + self._on_device_ready() + def _parse_macro(self, command): """ Parse a macro @@ -165,29 +198,12 @@ def _prepare_command(self, command): """ with self._mutex: command = command.replace("\n", "").replace("\r", "").upper() - - if command == " " or command == "": - return - return self._parse_macro(command) - def send_gcode_command(self, command, hide_command=False): - """ - Send the command - """ - command = self._prepare_command(command) - self.estimator.parse_command(command) - self._handle_send_command(command, hide_command) - self._update_timeout() # update the timeout because a new command has been sent - def _handle_send_command(self, command, hide_command=False): """ Send the gcode command to the device and handle the buffer """ - # wait until the lock for the buffer length is released - # if the lock is released means the board sent the ack for older lines and can send new ones - with self.buffer.get_buffer_wait_mutex(): - pass with self._mutex: # send the command after parsing the content @@ -271,6 +287,15 @@ def _on_readline(self, line): self.buffer.ack_received() return True + def _on_device_ready(self): + print("test") + """ + Called when the connected device is ready to receive commands + """ + with self._mutex: + self.event_handler.on_device_ready() + self._is_ready = True + def _log_received_line(self, line, hide_line=False): """ Log the line received from the device @@ -286,14 +311,6 @@ def _log_received_line(self, line, hide_line=False): # From here on the methods are abstract and must be implemented in the child class - @abstractmethod - def connect(self): - """ - Initialize the communication with the serial device - - Once the initializzation is done must set True the _is_ready flag - """ - @abstractmethod def emergency_stop(self): """ diff --git a/server/hw_controller/serial_device/firmwares/grbl.py b/server/hw_controller/serial_device/firmwares/grbl.py new file mode 100644 index 00000000..f41dc818 --- /dev/null +++ b/server/hw_controller/serial_device/firmwares/grbl.py @@ -0,0 +1,91 @@ +from threading import Thread +from time import time +from server.hw_controller.serial_device.device_serial import DeviceSerial +from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hw_controller.serial_device.firmwares.generic_firmware import GenericFirmware +from server.utils import settings_utils + + +class Grbl(GenericFirmware): + """ + Handle the comunication with devices running Grbl + """ + + def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler): + super().__init__(serial_settings, logger, event_handler) + self._logger.info("Setting up coms with a Grbl device") + # command used to update the buffer status or get a free ack + self.force_ack_command = "?" + + def emergency_stop(self): + """ + Stop the device immediately + """ + self.send_gcode_command("!") + + def _on_readline(self, line): + """ + Parse the line received from the device + + Args: + line: line to be parsed, received from the device usually + + Returns: + True if the line is handled correctly + """ + with self._mutex: + # if the line is not valid will return False + if not super()._on_readline(line): + return False + + hide_line = False + if line.startswith("<"): + try: + # interested in the "Bf:xx," part where xx is the content of the buffer + # select buffer content lines + res = line.split("Bf:")[1] + res = int(res.split(",")[0]) + if ( + res == 15 + ): # 15 => buffer is empty on the device (should include also 14 to make it more flexible?) + self.buffer.clear() + if res != 0: # 0 -> buffer is full + if len(self.buffer) > 0: + self.buffer.popleft() + hide_line = True + self.buffer.check_buffer_mutex_status() + + self._logger.log(settings_utils.LINE_SERVICE, line) + except: # sometimes may not receive the entire line thus it may throw an error + pass # FIXME + return False + # if the device is connected and ready will send a "Grbl" line + elif "Grbl" in line: + self._on_device_ready() + + # errors + elif "error:22" in line: + self.buffer.clear() + self._logger.error("Grbl error: {}".format(line)) + elif "error:" in line: + self._logger.error("Grbl error: {}".format(line)) + # TODO check/parse error types and give some hint about the problem? + + self._log_received_line(line, hide_line) + return True + + def _on_device_ready(self): + """ + Run some commands when the device is ready + """ + # grbl status report mask setup + # sandypi need to check the buffer to see if the machine has cleaned the buffer + # setup grbl to show the buffer status with the $10 command + # Grbl 1.1 https://github.com/gnea/grbl/wiki/Grbl-v1.1-Configuration + # Grbl 0.9 https://github.com/grbl/grbl/wiki/Configuring-Grbl-v0.9 + # to be compatible with both will send $10=6 (4(for v0.9) + 2(for v1.1)) + # the status will then be prompted with the "?" command when necessary + # the buffer will contain Bf:"usage of the buffer" + with self._mutex: + self.send_gcode_command("$10=6") + super()._on_device_ready() diff --git a/server/hw_controller/serial_device/firmwares/marlin.py b/server/hw_controller/serial_device/firmwares/marlin.py index 66dbd4e8..7a4139b7 100644 --- a/server/hw_controller/serial_device/firmwares/marlin.py +++ b/server/hw_controller/serial_device/firmwares/marlin.py @@ -9,49 +9,19 @@ class Marlin(GenericFirmware): """ - Handle devices running Marlin + Handle the comunication with devices running Marlin """ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler): super().__init__(serial_settings, logger, event_handler) - self._logger.info("Marlin device") + self._logger.info("Setting up coms with a Marlin device") # marlin specific values self._command_resolution = "{:.1f}" # command used to update the buffer status or get a free ack self.force_ack_command = "M114" - # ack string from the device - self.ack = "ok" # tolerance position (needed because the marlin rounding for the actual position is not the usual rounding) self.position_tolerance = 0.01 - def connect(self): - """ - Start the connection procedure with the serial device - """ - with self._mutex: - self._logger.info("Connecting to the serial device") - with self.serial_mutex: - self._serial_device = DeviceSerial( - self._serial_settings["serial_name"], - self._serial_settings["baudrate"], - self._logger.name, - ) - self._serial_device.set_on_readline_callback(self._on_readline) - self._serial_device.open() - # wait device ready - if self._serial_device.is_fake: - self._is_ready = True - else: - # runs a delay to wait the device to be ready - # TODO make this better: check messages from the device to understand when is ready - def delay(): - time.sleep(5) - self._on_device_ready() - - th = Thread(target=delay, daemon=True) - th.name = "waiting_device_ready" - th.start() - def emergency_stop(self): """ Stop the device immediately @@ -135,6 +105,9 @@ def _on_readline(self, line): if not self.buffer.is_empty(): hide_line = True + # the device send a "start" line when ready + elif "start" in line: + self._on_device_ready() # TODO check feedrate response for M220 and set feedrate # elif "_______" in line: # must see the real output from marlin @@ -187,6 +160,7 @@ def _on_device_ready(self): """ with self._mutex: self._reset_line_number() + super()._on_device_ready() def _reset_line_number(self, line_number=2): """ diff --git a/test.py b/test.py index 9e9d84f7..46db8ad8 100644 --- a/test.py +++ b/test.py @@ -1,12 +1,22 @@ # FIXME remove this import time +from server.hw_controller.serial_device.firmwares.grbl import Grbl from server.hw_controller.serial_device.firmwares.marlin import Marlin from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler import logging if __name__ == "__main__": + def test_device(device): + device.connect() + device.send_gcode_command("G28") + device.send_gcode_command("G0 X0 Y0 F300") + for x in range(15): + device.send_gcode_command(f"G0 X{x*2} Y0") + time.sleep(0.01) + time.sleep(5) + class EventHandler(FirwmareEventHandler): def on_line_received(self, line): print(f"Line received: {line}") @@ -14,20 +24,27 @@ def on_line_received(self, line): def on_line_sent(self, line): print(f"Line sent: {line}") + def on_device_ready(self): + print("Device ready") + + settings = {"serial_name": "COM3", "baudrate": 115200} + print("Testing Marlin") + + logger_name = logging.getLogger().name device = Marlin( - serial_settings={"serial_name": "COM3", "baudrate": 115200}, - logger=logging.getLogger().name, + serial_settings=settings, + logger=logger_name, event_handler=EventHandler(), ) - device.connect() - device.send_gcode_command("G28") - device.send_gcode_command("G0 X0 Y0 F300") - for x in range(15): - device.send_gcode_command(f"G0 X{x}00 Y0") - time.sleep(0.01) - time.sleep(10) - - print("Done") - - while True: - pass + + test_device(device) + + print("Marlin done") + + print("Testing grbl") + + device = Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler()) + + test_device(device) + + print("Grbl done") From 79303828ec71dab0c46fb0f665f84d87b95cde3b Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Tue, 1 Feb 2022 23:05:08 +0100 Subject: [PATCH 09/52] Preparing firmwares tests. For some reason they are not working as expected --- requirements.txt | 1 + server/hw_controller/serial_device/feeder.py | 8 ++- server/tests/test_z_firmwares.py | 51 ++++++++++++++++++++ test.py | 5 +- 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 server/tests/test_z_firmwares.py diff --git a/requirements.txt b/requirements.txt index 7280afae..e19a5440 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,6 +43,7 @@ pylint-plugin-utils==0.7 pyparsing==3.0.6 pyserial==3.5 pytest==6.2.5 +pytest-timeout==2.1.0 python-dateutil==2.8.2 python-dotenv==0.19.2 python-editor==1.0.4 diff --git a/server/hw_controller/serial_device/feeder.py b/server/hw_controller/serial_device/feeder.py index 8efdb99a..2177640e 100644 --- a/server/hw_controller/serial_device/feeder.py +++ b/server/hw_controller/serial_device/feeder.py @@ -3,4 +3,10 @@ def __init__(self): pass def get_status(self): - return {"is_running": True} + return {"is_running": True, "is_paused": True} + + def stop(self): + pass + + def resume(self): + pass diff --git a/server/tests/test_z_firmwares.py b/server/tests/test_z_firmwares.py new file mode 100644 index 00000000..7603c3e1 --- /dev/null +++ b/server/tests/test_z_firmwares.py @@ -0,0 +1,51 @@ +""" +Test firmware comunication control classes +""" + +import logging +from time import time + +import pytest +from server.hw_controller.serial_device.firmwares.grbl import Grbl +from server.hw_controller.serial_device.firmwares.marlin import Marlin +from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler + + +class EventHandler(FirwmareEventHandler): + def on_line_received(self, line): + pass + + def on_line_sent(self, line): + pass + + def on_device_ready(self): + print("Device ready") + + +settings = {"serial_name": "COM3", "baudrate": 115200} + +logger_name = logging.getLogger().name + + +def run_test(device): + + device.connect() + device.send_gcode_command("G28") + device.send_gcode_command("G0 X0 Y0 F300") + for x in range(15): + device.send_gcode_command(f"G0 X{x*2} Y0") + print("Done") + time.sleep(10) + assert len(device.buffer) == 0 + + +# setting up a maximum expected time of execution for this test +def test_marlin(): + pass + # run_test(Marlin(serial_settings=settings, logger=logger_name, event_handler=EventHandler())) + + +# setting up a maximum expected time of execution for this test +def test_grbl(): + pass + # run_test(Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler())) diff --git a/test.py b/test.py index 46db8ad8..ad7a068b 100644 --- a/test.py +++ b/test.py @@ -14,8 +14,9 @@ def test_device(device): device.send_gcode_command("G0 X0 Y0 F300") for x in range(15): device.send_gcode_command(f"G0 X{x*2} Y0") - time.sleep(0.01) - time.sleep(5) + print("Done") + time.sleep(6) + assert len(device.buffer) == 0 class EventHandler(FirwmareEventHandler): def on_line_received(self, line): From 086f16997444fd916b8dccff685d747193137845 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Wed, 2 Feb 2022 20:45:55 +0100 Subject: [PATCH 10/52] Fixing firmware tests --- server/tests/test_z_firmwares.py | 41 ++++++++++++++++++++------------ test.py | 4 ++-- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/server/tests/test_z_firmwares.py b/server/tests/test_z_firmwares.py index 7603c3e1..ac54124c 100644 --- a/server/tests/test_z_firmwares.py +++ b/server/tests/test_z_firmwares.py @@ -3,9 +3,7 @@ """ import logging -from time import time -import pytest from server.hw_controller.serial_device.firmwares.grbl import Grbl from server.hw_controller.serial_device.firmwares.marlin import Marlin from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler @@ -27,25 +25,38 @@ def on_device_ready(self): logger_name = logging.getLogger().name -def run_test(device): - +def run_test(device, fast_mode=False): device.connect() + device.fast_mode = fast_mode device.send_gcode_command("G28") - device.send_gcode_command("G0 X0 Y0 F300") + device.send_gcode_command("G0 X0 Y0 F3000") for x in range(15): - device.send_gcode_command(f"G0 X{x*2} Y0") - print("Done") - time.sleep(10) - assert len(device.buffer) == 0 + device.send_gcode_command(f"G0 X{x} Y0") + return True -# setting up a maximum expected time of execution for this test def test_marlin(): - pass - # run_test(Marlin(serial_settings=settings, logger=logger_name, event_handler=EventHandler())) + """ + Test Marlin firmware manager + """ + assert run_test( + Marlin(serial_settings=settings, logger=logger_name, event_handler=EventHandler()) + ) + + assert run_test( + Marlin(serial_settings=settings, logger=logger_name, event_handler=EventHandler()), + fast_mode=True, + ) -# setting up a maximum expected time of execution for this test def test_grbl(): - pass - # run_test(Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler())) + """ + Test Grbl firmware manager + """ + assert run_test( + Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler()) + ) + assert run_test( + Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler()), + fast_mode=True, + ) diff --git a/test.py b/test.py index ad7a068b..b22b1ae8 100644 --- a/test.py +++ b/test.py @@ -15,7 +15,7 @@ def test_device(device): for x in range(15): device.send_gcode_command(f"G0 X{x*2} Y0") print("Done") - time.sleep(6) + # time.sleep(6) assert len(device.buffer) == 0 class EventHandler(FirwmareEventHandler): @@ -46,6 +46,6 @@ def on_device_ready(self): device = Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler()) - test_device(device) + # test_device(device) print("Grbl done") From 3da68209d740dea883f375e497442d76d4ac6db7 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 3 Feb 2022 20:56:19 +0100 Subject: [PATCH 11/52] Refactoring hardware related folders --- server/__init__.py | 8 +-- .../{hw_controller => hardware}/__init__.py | 0 .../buttons/__init__.py | 0 .../buttons/actions.py | 2 +- .../buttons/buttons_manager.py | 62 +++++++++++++------ .../buttons/generic_button_event.py | 0 .../device}/__init__.py | 0 .../device/comunication}/device_serial.py | 2 +- .../device/comunication}/emulator.py | 0 .../device}/estimation/__init__.py | 0 .../device}/estimation/cartesian.py | 0 .../device}/estimation/generic_estimator.py | 0 server/hardware/device/estimation/polar.py | 0 server/hardware/device/estimation/scara.py | 0 .../device}/feeder.py | 0 .../device}/feeder_event_handler.py | 0 .../device}/firmwares/__init__.py | 0 .../device}/firmwares/commands_buffer.py | 0 .../firmwares/firmware_event_handler.py | 0 .../device}/firmwares/generic_firmware.py | 8 +-- .../device}/firmwares/grbl.py | 7 +-- .../device}/firmwares/marlin.py | 7 +-- .../device}/gcode_rescalers.py | 0 .../device}/old/feeder.py | 8 +-- .../device}/old/firmware_defaults.py | 0 .../feeder_event_manager.py | 2 +- .../leds/__init__.py | 0 .../leds/leds_animators/__init__.py | 0 .../leds/leds_controller.py | 50 +++++++++------ .../leds/leds_types/RGBW_neopixels.py | 29 +++++---- .../leds/leds_types/RGB_neopixels.py | 23 ++++--- .../hardware/leds/leds_types/WWA_neopixels.py | 19 ++++++ .../leds/leds_types/__init__.py | 0 .../leds/leds_types/dimmable.py | 22 ++++--- .../leds/leds_types/generic_LED_driver.py | 0 .../leds/light_sensors/__init__.py | 0 .../light_sensors/generic_light_sensor.py | 0 .../leds/light_sensors/tsl2591.py | 16 +++-- .../queue_manager.py | 0 .../leds/leds_types/WWA_neopixels.py | 18 ------ server/tests/test_buttons.py | 22 ++++--- server/tests/test_leds.py | 15 +++-- server/tests/test_z_firmwares.py | 6 +- test.py | 6 +- 44 files changed, 194 insertions(+), 138 deletions(-) rename server/{hw_controller => hardware}/__init__.py (100%) rename server/{hw_controller => hardware}/buttons/__init__.py (100%) rename server/{hw_controller => hardware}/buttons/actions.py (97%) rename server/{hw_controller => hardware}/buttons/buttons_manager.py (63%) rename server/{hw_controller => hardware}/buttons/generic_button_event.py (100%) rename server/{hw_controller/serial_device => hardware/device}/__init__.py (100%) rename server/{hw_controller/serial_device => hardware/device/comunication}/device_serial.py (98%) rename server/{hw_controller/serial_device => hardware/device/comunication}/emulator.py (100%) rename server/{hw_controller/serial_device => hardware/device}/estimation/__init__.py (100%) rename server/{hw_controller/serial_device => hardware/device}/estimation/cartesian.py (100%) rename server/{hw_controller/serial_device => hardware/device}/estimation/generic_estimator.py (100%) create mode 100644 server/hardware/device/estimation/polar.py create mode 100644 server/hardware/device/estimation/scara.py rename server/{hw_controller/serial_device => hardware/device}/feeder.py (100%) rename server/{hw_controller/serial_device => hardware/device}/feeder_event_handler.py (100%) rename server/{hw_controller/serial_device => hardware/device}/firmwares/__init__.py (100%) rename server/{hw_controller/serial_device => hardware/device}/firmwares/commands_buffer.py (100%) rename server/{hw_controller/serial_device => hardware/device}/firmwares/firmware_event_handler.py (100%) rename server/{hw_controller/serial_device => hardware/device}/firmwares/generic_firmware.py (96%) rename server/{hw_controller/serial_device => hardware/device}/firmwares/grbl.py (91%) rename server/{hw_controller/serial_device => hardware/device}/firmwares/marlin.py (95%) rename server/{hw_controller/serial_device => hardware/device}/gcode_rescalers.py (100%) rename server/{hw_controller/serial_device => hardware/device}/old/feeder.py (99%) rename server/{hw_controller/serial_device => hardware/device}/old/firmware_defaults.py (100%) rename server/{hw_controller => hardware}/feeder_event_manager.py (95%) rename server/{hw_controller => hardware}/leds/__init__.py (100%) rename server/{hw_controller => hardware}/leds/leds_animators/__init__.py (100%) rename server/{hw_controller => hardware}/leds/leds_controller.py (83%) rename server/{hw_controller => hardware}/leds/leds_types/RGBW_neopixels.py (64%) rename server/{hw_controller => hardware}/leds/leds_types/RGB_neopixels.py (72%) create mode 100644 server/hardware/leds/leds_types/WWA_neopixels.py rename server/{hw_controller => hardware}/leds/leds_types/__init__.py (100%) rename server/{hw_controller => hardware}/leds/leds_types/dimmable.py (63%) rename server/{hw_controller => hardware}/leds/leds_types/generic_LED_driver.py (100%) rename server/{hw_controller => hardware}/leds/light_sensors/__init__.py (100%) rename server/{hw_controller => hardware}/leds/light_sensors/generic_light_sensor.py (100%) rename server/{hw_controller => hardware}/leds/light_sensors/tsl2591.py (69%) rename server/{hw_controller => hardware}/queue_manager.py (100%) delete mode 100644 server/hw_controller/leds/leds_types/WWA_neopixels.py diff --git a/server/__init__.py b/server/__init__.py index 53c26d41..198f2dcd 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -99,11 +99,11 @@ def base_static(filename): import server.api.drawings from server.sockets_interface.socketio_emits import SocketioEmits import server.sockets_interface.socketio_callbacks - from server.hw_controller.serial_device.feeder import Feeder - from server.hw_controller.queue_manager import QueueManager + from server.hardware.device.feeder import Feeder + from server.hardware.queue_manager import QueueManager from server.preprocessing.file_observer import GcodeObserverManager - from server.hw_controller.leds.leds_controller import LedsController - from server.hw_controller.buttons.buttons_manager import ButtonsManager + from server.hardware.leds.leds_controller import LedsController + from server.hardware.buttons.buttons_manager import ButtonsManager from server.utils.stats import StatsManager except Exception as e: diff --git a/server/hw_controller/__init__.py b/server/hardware/__init__.py similarity index 100% rename from server/hw_controller/__init__.py rename to server/hardware/__init__.py diff --git a/server/hw_controller/buttons/__init__.py b/server/hardware/buttons/__init__.py similarity index 100% rename from server/hw_controller/buttons/__init__.py rename to server/hardware/buttons/__init__.py diff --git a/server/hw_controller/buttons/actions.py b/server/hardware/buttons/actions.py similarity index 97% rename from server/hw_controller/buttons/actions.py rename to server/hardware/buttons/actions.py index 7f5506ea..53bdbf10 100644 --- a/server/hw_controller/buttons/actions.py +++ b/server/hardware/buttons/actions.py @@ -3,7 +3,7 @@ # in this file are defined the events that can be associated to a button -from server.hw_controller.buttons.generic_button_event import GenericButtonAction +from server.hardware.buttons.generic_button_event import GenericButtonAction class StartPause(GenericButtonAction): diff --git a/server/hw_controller/buttons/buttons_manager.py b/server/hardware/buttons/buttons_manager.py similarity index 63% rename from server/hw_controller/buttons/buttons_manager.py rename to server/hardware/buttons/buttons_manager.py index 7e32a133..999c807a 100644 --- a/server/hw_controller/buttons/buttons_manager.py +++ b/server/hardware/buttons/buttons_manager.py @@ -1,23 +1,29 @@ -# this class gathers all the classes from the actions.py file and share them with the frontend +# this class gathers all the classes from the actions.py file and share them with the frontend # (this is to avoid doubling the data on server side and frontend) # when a button is set, the same class will listen on the GPIO for the button actions import inspect -from server.hw_controller.buttons.generic_button_event import GenericButtonAction, GenericButtonEventManager -import server.hw_controller.buttons.actions as button_actions +from server.hardware.buttons.generic_button_event import ( + GenericButtonAction, + GenericButtonEventManager, +) +import server.hardware.buttons.actions as button_actions from server.utils.settings_utils import load_settings BOUNCE_TIME = 100 -class ButtonsManager: +class ButtonsManager: def __init__(self, app): self.app = app try: import RPi.GPIO as GPIO + self._gpio_available = True except (RuntimeError, ModuleNotFoundError): - self.app.logger.error("buttons:The GPIO is not accessible. If you are using a raspberry pi be sure to use superuser privileges to run this software. \n Be sure to check also the installation instructions dedicated to the hw options\n If the error persist open an issue on github") + self.app.logger.error( + "buttons:The GPIO is not accessible. If you are using a raspberry pi be sure to use superuser privileges to run this software. \n Be sure to check also the installation instructions dedicated to the hw options\n If the error persist open an issue on github" + ) self._gpio_available = False # loading actions @@ -28,16 +34,25 @@ def __init__(self, app): for cl in inspect.getmembers(button_actions, inspect.isclass): if not cl[1] == GenericButtonAction: # preparing array for the frontend (will be jsonized) - self.available_buttons_actions.append({"description": cl[1].description, "label": cl[1].label, "name": cl[0], "usage": cl[1].usage}) - self._actions[cl[0]] = cl[1] # storing classes in an array to instantiate the actions later - self._labels[cl[1].label] = cl[0] # creating a map from class label to class - + self.available_buttons_actions.append( + { + "description": cl[1].description, + "label": cl[1].label, + "name": cl[0], + "usage": cl[1].usage, + } + ) + self._actions[cl[0]] = cl[ + 1 + ] # storing classes in an array to instantiate the actions later + self._labels[cl[1].label] = cl[0] # creating a map from class label to class + self.update_settings(load_settings()) - + def get_buttons_options(self): # TODO filter actions that are not available (like leds brightness/control if the leds are not available) return self.available_buttons_actions - + def update_settings(self, settings): if self.gpio_is_available(): settings = settings["buttons"] @@ -45,13 +60,16 @@ def update_settings(self, settings): if not self._buttons is None: pairs = zip(settings["buttons"], self._buttons) # if something changed, reload all the buttons callbacks - if not any(x != y for x, y in pairs): # should update only if any difference has been found or if the self._button object is None + if not any( + x != y for x, y in pairs + ): # should update only if any difference has been found or if the self._button object is None should_update = False - + if not should_update: return - import RPi.GPIO as GPIO + import RPi.GPIO as GPIO + if not self._buttons is None: # clear GPIO for b in self._buttons: @@ -62,17 +80,19 @@ def update_settings(self, settings): GPIO.cleanup() GPIO.setmode(GPIO.BCM) self._buttons = settings["buttons"] - # set new callbacks + # set new callbacks for b in self._buttons: try: pin = int(b["pin"]["value"]) except: self.app.logger.error("Check the button pin number. Looks like is not a number") - + bobj = GenericButtonEventManager(self.app, pin) # adding actions to the generic button event manager bobj.set_click_action(self._actions[self._labels[b["click"]["value"]]](self.app)) - bobj.set_long_press_action(self._actions[self._labels[b["press"]["value"]]](self.app)) + bobj.set_long_press_action( + self._actions[self._labels[b["press"]["value"]]](self.app) + ) pull_up_down = None if b["pull"]["value"] == "Pullup internal": @@ -83,9 +103,11 @@ def update_settings(self, settings): if pull_up_down is None: GPIO.setup(pin, GPIO.IN) - else: + else: GPIO.setup(pin, GPIO.IN, pull_up_down=pull_up_down) - GPIO.add_event_detect(pin, GPIO.BOTH, callback = bobj.button_change, bouncetime = BOUNCE_TIME) # the rising or falling edge is detected in the button event class - + GPIO.add_event_detect( + pin, GPIO.BOTH, callback=bobj.button_change, bouncetime=BOUNCE_TIME + ) # the rising or falling edge is detected in the button event class + def gpio_is_available(self): return self._gpio_available diff --git a/server/hw_controller/buttons/generic_button_event.py b/server/hardware/buttons/generic_button_event.py similarity index 100% rename from server/hw_controller/buttons/generic_button_event.py rename to server/hardware/buttons/generic_button_event.py diff --git a/server/hw_controller/serial_device/__init__.py b/server/hardware/device/__init__.py similarity index 100% rename from server/hw_controller/serial_device/__init__.py rename to server/hardware/device/__init__.py diff --git a/server/hw_controller/serial_device/device_serial.py b/server/hardware/device/comunication/device_serial.py similarity index 98% rename from server/hw_controller/serial_device/device_serial.py rename to server/hardware/device/comunication/device_serial.py index 21cf46cf..d13179c5 100644 --- a/server/hw_controller/serial_device/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -5,7 +5,7 @@ import logging import glob -from server.hw_controller.serial_device.emulator import Emulator +from server.hardware.device.emulator import Emulator class DeviceSerial: diff --git a/server/hw_controller/serial_device/emulator.py b/server/hardware/device/comunication/emulator.py similarity index 100% rename from server/hw_controller/serial_device/emulator.py rename to server/hardware/device/comunication/emulator.py diff --git a/server/hw_controller/serial_device/estimation/__init__.py b/server/hardware/device/estimation/__init__.py similarity index 100% rename from server/hw_controller/serial_device/estimation/__init__.py rename to server/hardware/device/estimation/__init__.py diff --git a/server/hw_controller/serial_device/estimation/cartesian.py b/server/hardware/device/estimation/cartesian.py similarity index 100% rename from server/hw_controller/serial_device/estimation/cartesian.py rename to server/hardware/device/estimation/cartesian.py diff --git a/server/hw_controller/serial_device/estimation/generic_estimator.py b/server/hardware/device/estimation/generic_estimator.py similarity index 100% rename from server/hw_controller/serial_device/estimation/generic_estimator.py rename to server/hardware/device/estimation/generic_estimator.py diff --git a/server/hardware/device/estimation/polar.py b/server/hardware/device/estimation/polar.py new file mode 100644 index 00000000..e69de29b diff --git a/server/hardware/device/estimation/scara.py b/server/hardware/device/estimation/scara.py new file mode 100644 index 00000000..e69de29b diff --git a/server/hw_controller/serial_device/feeder.py b/server/hardware/device/feeder.py similarity index 100% rename from server/hw_controller/serial_device/feeder.py rename to server/hardware/device/feeder.py diff --git a/server/hw_controller/serial_device/feeder_event_handler.py b/server/hardware/device/feeder_event_handler.py similarity index 100% rename from server/hw_controller/serial_device/feeder_event_handler.py rename to server/hardware/device/feeder_event_handler.py diff --git a/server/hw_controller/serial_device/firmwares/__init__.py b/server/hardware/device/firmwares/__init__.py similarity index 100% rename from server/hw_controller/serial_device/firmwares/__init__.py rename to server/hardware/device/firmwares/__init__.py diff --git a/server/hw_controller/serial_device/firmwares/commands_buffer.py b/server/hardware/device/firmwares/commands_buffer.py similarity index 100% rename from server/hw_controller/serial_device/firmwares/commands_buffer.py rename to server/hardware/device/firmwares/commands_buffer.py diff --git a/server/hw_controller/serial_device/firmwares/firmware_event_handler.py b/server/hardware/device/firmwares/firmware_event_handler.py similarity index 100% rename from server/hw_controller/serial_device/firmwares/firmware_event_handler.py rename to server/hardware/device/firmwares/firmware_event_handler.py diff --git a/server/hw_controller/serial_device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py similarity index 96% rename from server/hw_controller/serial_device/firmwares/generic_firmware.py rename to server/hardware/device/firmwares/generic_firmware.py index 94fe1507..a097c919 100644 --- a/server/hw_controller/serial_device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -5,11 +5,11 @@ from abc import ABC, abstractmethod from threading import RLock, Lock from py_expression_eval import Parser -from server.hw_controller.serial_device.device_serial import DeviceSerial +from server.hardware.device.comunication.device_serial import DeviceSerial -from server.hw_controller.serial_device.estimation.generic_estimator import GenericEstimator -from server.hw_controller.serial_device.firmwares.commands_buffer import CommandBuffer -from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hardware.device.estimation.generic_estimator import GenericEstimator +from server.hardware.device.firmwares.commands_buffer import CommandBuffer +from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler from server.utils import buffered_timeout, settings_utils # Defines the character used to define macros diff --git a/server/hw_controller/serial_device/firmwares/grbl.py b/server/hardware/device/firmwares/grbl.py similarity index 91% rename from server/hw_controller/serial_device/firmwares/grbl.py rename to server/hardware/device/firmwares/grbl.py index f41dc818..cddf9da2 100644 --- a/server/hw_controller/serial_device/firmwares/grbl.py +++ b/server/hardware/device/firmwares/grbl.py @@ -1,8 +1,5 @@ -from threading import Thread -from time import time -from server.hw_controller.serial_device.device_serial import DeviceSerial -from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler -from server.hw_controller.serial_device.firmwares.generic_firmware import GenericFirmware +from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hardware.device.firmwares.generic_firmware import GenericFirmware from server.utils import settings_utils diff --git a/server/hw_controller/serial_device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py similarity index 95% rename from server/hw_controller/serial_device/firmwares/marlin.py rename to server/hardware/device/firmwares/marlin.py index 7a4139b7..df70e4de 100644 --- a/server/hw_controller/serial_device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -1,10 +1,7 @@ from copy import deepcopy -from threading import Thread -import time -from server.hw_controller.serial_device.device_serial import DeviceSerial -from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler -from server.hw_controller.serial_device.firmwares.generic_firmware import GenericFirmware +from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hardware.device.firmwares.generic_firmware import GenericFirmware class Marlin(GenericFirmware): diff --git a/server/hw_controller/serial_device/gcode_rescalers.py b/server/hardware/device/gcode_rescalers.py similarity index 100% rename from server/hw_controller/serial_device/gcode_rescalers.py rename to server/hardware/device/gcode_rescalers.py diff --git a/server/hw_controller/serial_device/old/feeder.py b/server/hardware/device/old/feeder.py similarity index 99% rename from server/hw_controller/serial_device/old/feeder.py rename to server/hardware/device/old/feeder.py index 35bb317a..cf9d112d 100644 --- a/server/hw_controller/serial_device/old/feeder.py +++ b/server/hardware/device/old/feeder.py @@ -12,10 +12,10 @@ from server.utils import limited_size_dict, buffered_timeout, settings_utils from server.utils.logging_utils import formatter, MultiprocessRotatingFileHandler -from server.hw_controller.serial_device.feeder_event_handler import FeederEventHandler -from server.hw_controller.serial_device.device_serial import DeviceSerial -from server.hw_controller.serial_device.gcode_rescalers import Fit -import server.hw_controller.serial_device.firmware_defaults as firmware +from server.hardware.device.feeder_event_handler import FeederEventHandler +from server.hardware.device.device_serial import DeviceSerial +from server.hardware.device.gcode_rescalers import Fit +import server.hardware.device.firmware_defaults as firmware from server.database.playlist_elements import DrawingElement, TimeElement from server.database.generic_playlist_element import UNKNOWN_PROGRESS diff --git a/server/hw_controller/serial_device/old/firmware_defaults.py b/server/hardware/device/old/firmware_defaults.py similarity index 100% rename from server/hw_controller/serial_device/old/firmware_defaults.py rename to server/hardware/device/old/firmware_defaults.py diff --git a/server/hw_controller/feeder_event_manager.py b/server/hardware/feeder_event_manager.py similarity index 95% rename from server/hw_controller/feeder_event_manager.py rename to server/hardware/feeder_event_manager.py index f9678f6d..e9c915c4 100644 --- a/server/hw_controller/feeder_event_manager.py +++ b/server/hardware/feeder_event_manager.py @@ -1,5 +1,5 @@ from server.database.playlist_elements import DrawingElement -from server.hw_controller.serial_device.feeder_event_handler import FeederEventHandler +from server.hardware.device.feeder_event_handler import FeederEventHandler import time diff --git a/server/hw_controller/leds/__init__.py b/server/hardware/leds/__init__.py similarity index 100% rename from server/hw_controller/leds/__init__.py rename to server/hardware/leds/__init__.py diff --git a/server/hw_controller/leds/leds_animators/__init__.py b/server/hardware/leds/leds_animators/__init__.py similarity index 100% rename from server/hw_controller/leds/leds_animators/__init__.py rename to server/hardware/leds/leds_animators/__init__.py diff --git a/server/hw_controller/leds/leds_controller.py b/server/hardware/leds/leds_controller.py similarity index 83% rename from server/hw_controller/leds/leds_controller.py rename to server/hardware/leds/leds_controller.py index 092c82e1..b532bd6a 100644 --- a/server/hw_controller/leds/leds_controller.py +++ b/server/hardware/leds/leds_controller.py @@ -4,11 +4,12 @@ from server.utils import settings_utils -from server.hw_controller.leds.leds_types.dimmable import Dimmable -from server.hw_controller.leds.leds_types.RGB_neopixels import RGBNeopixels -from server.hw_controller.leds.leds_types.RGBW_neopixels import RGBWNeopixels -from server.hw_controller.leds.leds_types.WWA_neopixels import WWANeopixels -from server.hw_controller.leds.light_sensors.tsl2591 import TSL2591 +from server.hardware.leds.leds_types.dimmable import Dimmable +from server.hardware.leds.leds_types.RGB_neopixels import RGBNeopixels +from server.hardware.leds.leds_types.RGBW_neopixels import RGBWNeopixels +from server.hardware.leds.leds_types.WWA_neopixels import WWANeopixels +from server.hardware.leds.light_sensors.tsl2591 import TSL2591 + class LedsController: def __init__(self, app): @@ -19,7 +20,7 @@ def __init__(self, app): self._mutex = Lock() self._should_update = False self._running = False - self._color = (0,0,0,0) + self._color = (0, 0, 0, 0) self._brightness = 0 self._just_turned_on = True self.update_settings(settings_utils.load_settings()) @@ -36,10 +37,10 @@ def has_light_sensor(self): def start(self): if not self.driver is None: self._running = True - self._th = Thread(target = self._thf, daemon=True) + self._th = Thread(target=self._thf, daemon=True) self._th.name = "leds_controller" self._th.start() - + def stop(self): if not self.driver is None: with self._mutex: @@ -53,7 +54,7 @@ def decrease_brightness(self): def increase_brightness(self): if not self.driver is None: self.driver.increase_brightness() - + def fill(self, color): if not self.driver is None: self.driver.fill(color) @@ -63,11 +64,11 @@ def reset_lights(self): self.set_brightness(0) self.driver.fill_white() self._just_turned_on = True - + def _thf(self): self.app.logger.info("Leds controller started") try: - while(True): + while True: with self._mutex: if self._should_update: self.driver.fill(self._color) @@ -88,7 +89,7 @@ def set_color(self, color): g = int(color[3:5], 16) b = int(color[5:7], 16) w = 0 - if len(color)>7: + if len(color) > 7: w = int(color[7:9], 16) with self._mutex: self._color = (r, g, b, w) @@ -121,7 +122,11 @@ def update_settings(self, settings): self.stop() restart = True settings = DotMap(settings_utils.get_only_values(settings)) - dims = (int(settings.leds.width), int(settings.leds.height), int(settings.leds.circumference)) + dims = ( + int(settings.leds.width), + int(settings.leds.height), + int(settings.leds.circumference), + ) if self.dimensions != dims: self.dimensions = dims self.leds_type = None @@ -130,9 +135,13 @@ def update_settings(self, settings): self.pin = settings.leds.pin1 self.leds_type = settings.leds.type try: - # the leds number calculation depends on the type of table. + # the leds number calculation depends on the type of table. # If is square or rectangular should use a base and height, for round tables will use the total number of leds directly - leds_number = (int(self.dimensions[0]) + int(self.dimensions[1]))*2 if settings.device.type == "Cartesian" else int(self.dimensions[2]) + leds_number = ( + (int(self.dimensions[0]) + int(self.dimensions[1])) * 2 + if settings.device.type == "Cartesian" + else int(self.dimensions[2]) + ) leds_class = Dimmable if self.leds_type == "RGB": leds_class = RGBNeopixels @@ -140,14 +149,14 @@ def update_settings(self, settings): leds_class = RGBWNeopixels elif self.leds_type == "WWA": leds_class = WWANeopixels - + self.driver = leds_class(leds_number, settings.leds.pin1, logger=self.app.logger) - except Exception as e: + except Exception as e: self.driver = None self.app.semits.show_toast_on_UI("Led driver type not compatible with current HW") self.app.logger.exception(e) self.app.logger.error("Cannot initialize leds controller") - try: + try: if settings.leds.light_sensor == "TSL2591": self.sensor = TSL2591(self.app) else: @@ -155,11 +164,12 @@ def update_settings(self, settings): self.sensor.deinit() except Exception as e: if self.is_available(): - self.app.semits.show_toast_on_UI("The select sensor is not compatible with the current setup") + self.app.semits.show_toast_on_UI( + "The select sensor is not compatible with the current setup" + ) self.app.logger.error("Cannot initialize leds light sensor") self.app.logger.exception(e) if restart: self.start() self.reset_lights() - \ No newline at end of file diff --git a/server/hw_controller/leds/leds_types/RGBW_neopixels.py b/server/hardware/leds/leds_types/RGBW_neopixels.py similarity index 64% rename from server/hw_controller/leds/leds_types/RGBW_neopixels.py rename to server/hardware/leds/leds_types/RGBW_neopixels.py index 8ccf8d27..d5b6e19a 100644 --- a/server/hw_controller/leds/leds_types/RGBW_neopixels.py +++ b/server/hardware/leds/leds_types/RGBW_neopixels.py @@ -1,17 +1,18 @@ -from server.hw_controller.leds.leds_types.generic_LED_driver import GenericLedDriver +from server.hardware.leds.leds_types.generic_LED_driver import GenericLedDriver + class RGBWNeopixels(GenericLedDriver): def __init__(self, leds_number, bcm_pin, *argvs, **kargvs): kargvs["colors"] = 4 super().__init__(leds_number, bcm_pin, *argvs, **kargvs) - + def fill(self, color): - self._original_colors[:] = [color]*self.leds_number + self._original_colors[:] = [color] * self.leds_number self.pixels.fill(self._normalize_color(color)) - + def fill_white(self): - self.fill((0,0,0,255)) - + self.fill((0, 0, 0, 255)) + # abstract methods overwrites def deinit(self): @@ -23,7 +24,10 @@ def init_pixels(self): import board import neopixel from adafruit_blinka.microcontroller.bcm283x.pin import Pin - self.pixels = neopixel.NeoPixel(Pin(self.pin), self.leds_number, pixel_order = neopixel.GRBW) + + self.pixels = neopixel.NeoPixel( + Pin(self.pin), self.leds_number, pixel_order=neopixel.GRBW + ) # turn off all leds self.clear() except: @@ -32,11 +36,12 @@ def init_pixels(self): if __name__ == "__main__": from time import sleep - leds = RGBWNeopixels(5,18) - leds.fill((100,0,0,0)) - leds[0] = (0,10,0,0) - leds[1] = (0,0,10,0) - leds[2] = (0,0,0,10) + + leds = RGBWNeopixels(5, 18) + leds.fill((100, 0, 0, 0)) + leds[0] = (0, 10, 0, 0) + leds[1] = (0, 0, 10, 0) + leds[2] = (0, 0, 0, 10) sleep(2) leds.deinit() diff --git a/server/hw_controller/leds/leds_types/RGB_neopixels.py b/server/hardware/leds/leds_types/RGB_neopixels.py similarity index 72% rename from server/hw_controller/leds/leds_types/RGB_neopixels.py rename to server/hardware/leds/leds_types/RGB_neopixels.py index 686318b0..83dfdbd1 100644 --- a/server/hw_controller/leds/leds_types/RGB_neopixels.py +++ b/server/hardware/leds/leds_types/RGB_neopixels.py @@ -1,14 +1,15 @@ -from server.hw_controller.leds.leds_types.generic_LED_driver import GenericLedDriver +from server.hardware.leds.leds_types.generic_LED_driver import GenericLedDriver + class RGBNeopixels(GenericLedDriver): def __init__(self, leds_number, bcm_pin, *argvs, **kargvs): kargvs["colors"] = 3 - super().__init__(leds_number, bcm_pin, *argvs, **kargvs) + super().__init__(leds_number, bcm_pin, *argvs, **kargvs) def fill(self, color): - self._original_colors[:] = [color]*self.leds_number + self._original_colors[:] = [color] * self.leds_number self.pixels.fill(self._normalize_color(color)) - + # abstract methods overwrite def deinit(self): @@ -20,20 +21,22 @@ def init_pixels(self): import board import neopixel from adafruit_blinka.microcontroller.bcm283x.pin import Pin + self.pixels = neopixel.NeoPixel(Pin(self.pin), self.leds_number) # turn off all leds self.clear() except: - raise ModuleNotFoundError("Cannot find the libraries to control the selected hardware") + raise ModuleNotFoundError("Cannot find the libraries to control the selected hardware") if __name__ == "__main__": from time import sleep - leds = RGBNeopixels(5,18) - leds.fill((100,0,0)) - leds[0] = (10,0,0) - leds[1] = (0,10,0) - leds[2] = (0,0,10) + + leds = RGBNeopixels(5, 18) + leds.fill((100, 0, 0)) + leds[0] = (10, 0, 0) + leds[1] = (0, 10, 0) + leds[2] = (0, 0, 10) sleep(2) leds.deinit() diff --git a/server/hardware/leds/leds_types/WWA_neopixels.py b/server/hardware/leds/leds_types/WWA_neopixels.py new file mode 100644 index 00000000..ba6e5665 --- /dev/null +++ b/server/hardware/leds/leds_types/WWA_neopixels.py @@ -0,0 +1,19 @@ +from server.hardware.leds.leds_types.generic_LED_driver import GenericLedDriver +from server.hardware.leds.leds_types.RGB_neopixels import RGBNeopixels + +# WWA leds are RGB leds with different colors (R -> amber, G -> cold white, B -> warm white) +class WWANeopixels(RGBNeopixels): + pass + + +if __name__ == "__main__": + from time import sleep + + leds = WWANeopixels(5, 18) + leds.fill((100, 0, 0)) + leds[0] = (10, 0, 0) + leds[1] = (0, 10, 0) + leds[2] = (0, 0, 10) + sleep(2) + + leds.deinit() diff --git a/server/hw_controller/leds/leds_types/__init__.py b/server/hardware/leds/leds_types/__init__.py similarity index 100% rename from server/hw_controller/leds/leds_types/__init__.py rename to server/hardware/leds/leds_types/__init__.py diff --git a/server/hw_controller/leds/leds_types/dimmable.py b/server/hardware/leds/leds_types/dimmable.py similarity index 63% rename from server/hw_controller/leds/leds_types/dimmable.py rename to server/hardware/leds/leds_types/dimmable.py index 0a685d5f..cd02f70f 100644 --- a/server/hw_controller/leds/leds_types/dimmable.py +++ b/server/hardware/leds/leds_types/dimmable.py @@ -1,22 +1,23 @@ from statistics import mean -from server.hw_controller.leds.leds_types.generic_LED_driver import GenericLedDriver +from server.hardware.leds.leds_types.generic_LED_driver import GenericLedDriver + class Dimmable(GenericLedDriver): def __init__(self, leds_number, bcm_pin, *argvs, **kargvs): super().__init__(leds_number, bcm_pin, colors=1, *argvs, **kargvs) - + def fill(self, color): - self._original_colors[:] = [color]*self.leds_number - val = int(mean(color)/2.55) # (mean/255)*100 + self._original_colors[:] = [color] * self.leds_number + val = int(mean(color) / 2.55) # (mean/255)*100 self.pwm.ChangeDutyCycle(val) self.pixels[:] = color - + def __setitem__(self, key, color): - val = int(mean(color)/2.55) # (mean/255)*100 + val = int(mean(color) / 2.55) # (mean/255)*100 self.pwm.ChangeDutyCycle(val) self.pixels[key] = color - self._original_colors[:] = [color]*self.leds_number - + self._original_colors[:] = [color] * self.leds_number + # abstract methods overrides def deinit(self): @@ -25,9 +26,10 @@ def deinit(self): def init_pixels(self): try: import RPi.GPIO as GPIO + GPIO.setmode(GPIO.BCM) - GPIO.setup(self.pin, GPIO.OUT) + GPIO.setup(self.pin, GPIO.OUT) self.pwm = GPIO.PWM(self.pin, 100) self.pwm.start(0) except (RuntimeError, ModuleNotFoundError) as e: - raise \ No newline at end of file + raise diff --git a/server/hw_controller/leds/leds_types/generic_LED_driver.py b/server/hardware/leds/leds_types/generic_LED_driver.py similarity index 100% rename from server/hw_controller/leds/leds_types/generic_LED_driver.py rename to server/hardware/leds/leds_types/generic_LED_driver.py diff --git a/server/hw_controller/leds/light_sensors/__init__.py b/server/hardware/leds/light_sensors/__init__.py similarity index 100% rename from server/hw_controller/leds/light_sensors/__init__.py rename to server/hardware/leds/light_sensors/__init__.py diff --git a/server/hw_controller/leds/light_sensors/generic_light_sensor.py b/server/hardware/leds/light_sensors/generic_light_sensor.py similarity index 100% rename from server/hw_controller/leds/light_sensors/generic_light_sensor.py rename to server/hardware/leds/light_sensors/generic_light_sensor.py diff --git a/server/hw_controller/leds/light_sensors/tsl2591.py b/server/hardware/leds/light_sensors/tsl2591.py similarity index 69% rename from server/hw_controller/leds/light_sensors/tsl2591.py rename to server/hardware/leds/light_sensors/tsl2591.py index 4bd6fb11..47633d68 100644 --- a/server/hw_controller/leds/light_sensors/tsl2591.py +++ b/server/hardware/leds/light_sensors/tsl2591.py @@ -1,17 +1,19 @@ -from server.hw_controller.leds.light_sensors.generic_light_sensor import GenericLightSensor +from server.hardware.leds.light_sensors.generic_light_sensor import GenericLightSensor from math import sqrt LUX_MAX = 30 BRIGHTNESS_MIN = 0.05 + class TSL2591(GenericLightSensor): """Light sensor based on TSL2519 (I2C) sensor""" def __init__(self, app): super().__init__(app) - try: + try: import board import adafruit_tsl2591 + i2c = board.I2C() self._sensor = adafruit_tsl2591.TSL2591(i2c) except: @@ -19,10 +21,12 @@ def __init__(self, app): def get_brightness(self): lux = self._sensor.lux - tmp = max(sqrt(min(lux, LUX_MAX)/LUX_MAX), BRIGHTNESS_MIN) # calculating the brightness to use - self.app.logger.info("Sensor light intensity: {} lux".format(lux)) # FIXME remove this - self.app.logger.info("Sensor current brightness: {}".format(tmp)) # FIXME remove this - return tmp + tmp = max( + sqrt(min(lux, LUX_MAX) / LUX_MAX), BRIGHTNESS_MIN + ) # calculating the brightness to use + self.app.logger.info("Sensor light intensity: {} lux".format(lux)) # FIXME remove this + self.app.logger.info("Sensor current brightness: {}".format(tmp)) # FIXME remove this + return tmp def is_connected(self): return not self._sensor is None diff --git a/server/hw_controller/queue_manager.py b/server/hardware/queue_manager.py similarity index 100% rename from server/hw_controller/queue_manager.py rename to server/hardware/queue_manager.py diff --git a/server/hw_controller/leds/leds_types/WWA_neopixels.py b/server/hw_controller/leds/leds_types/WWA_neopixels.py deleted file mode 100644 index d34ffdf6..00000000 --- a/server/hw_controller/leds/leds_types/WWA_neopixels.py +++ /dev/null @@ -1,18 +0,0 @@ -from server.hw_controller.leds.leds_types.generic_LED_driver import GenericLedDriver -from server.hw_controller.leds.leds_types.RGB_neopixels import RGBNeopixels - -# WWA leds are RGB leds with different colors (R -> amber, G -> cold white, B -> warm white) -class WWANeopixels(RGBNeopixels): - pass - - -if __name__ == "__main__": - from time import sleep - leds = WWANeopixels(5,18) - leds.fill((100,0,0)) - leds[0] = (10,0,0) - leds[1] = (0,10,0) - leds[2] = (0,0,10) - sleep(2) - - leds.deinit() diff --git a/server/tests/test_buttons.py b/server/tests/test_buttons.py index 1f3619af..13f09d3a 100644 --- a/server/tests/test_buttons.py +++ b/server/tests/test_buttons.py @@ -1,8 +1,9 @@ import inspect from server import app -from server.hw_controller.buttons.generic_button_event import GenericButtonAction -import server.hw_controller.buttons.actions as button_actions +from server.hardware.buttons.generic_button_event import GenericButtonAction +import server.hardware.buttons.actions as button_actions + def test_buttons_get_options(): options = app.bmanager.get_buttons_options() @@ -10,14 +11,21 @@ def test_buttons_get_options(): for o in options: for f in fields: if not f in o.keys(): - assert(False) - if o["description"] == GenericButtonAction.description or o["label"] == GenericButtonAction.label: - assert(False) + assert False + if ( + o["description"] == GenericButtonAction.description + or o["label"] == GenericButtonAction.label + ): + assert False + def test_buttons_gpio_available(): - assert(not app.bmanager.gpio_is_available()) # the test must pass on a linux device not using real hw + assert ( + not app.bmanager.gpio_is_available() + ) # the test must pass on a linux device not using real hw + -# checking if the button actions are created correctly and also if the execute method has been overwritten +# checking if the button actions are created correctly and also if the execute method has been overwritten def test_buttons_action_has_execute(): for cl in inspect.getmembers(button_actions, inspect.isclass): if not cl[1] is GenericButtonAction: diff --git a/server/tests/test_leds.py b/server/tests/test_leds.py index 8db6cd84..ae744799 100644 --- a/server/tests/test_leds.py +++ b/server/tests/test_leds.py @@ -1,26 +1,33 @@ -from server.hw_controller.leds.leds_controller import LedsController +from server.hardware.leds.leds_controller import LedsController from server import app # cannot really test if the leds are working # just checking that even without the correct hw the module is not breaking the sw + def test_led_driver_available(): - assert(not app.lmanager.is_available()) # the test should work on a linux server without hw leds + assert not app.lmanager.is_available() # the test should work on a linux server without hw leds + def test_led_driver_start(): app.lmanager.start() + def test_led_driver_stop(): app.lmanager.stop() + def test_led_driver_has_light_sensor(): - assert(not app.lmanager.has_light_sensor()) + assert not app.lmanager.has_light_sensor() + def test_led_reset_lights(): app.lmanager.reset_lights() + def test_led_increase_brightness(): app.lmanager.increase_brightness() + def test_led_decrease_brightness(): - app.lmanager.decrease_brightness() \ No newline at end of file + app.lmanager.decrease_brightness() diff --git a/server/tests/test_z_firmwares.py b/server/tests/test_z_firmwares.py index ac54124c..29cc5457 100644 --- a/server/tests/test_z_firmwares.py +++ b/server/tests/test_z_firmwares.py @@ -4,9 +4,9 @@ import logging -from server.hw_controller.serial_device.firmwares.grbl import Grbl -from server.hw_controller.serial_device.firmwares.marlin import Marlin -from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hardware.device.firmwares.grbl import Grbl +from server.hardware.device.firmwares.marlin import Marlin +from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler class EventHandler(FirwmareEventHandler): diff --git a/test.py b/test.py index b22b1ae8..ac0418a7 100644 --- a/test.py +++ b/test.py @@ -1,9 +1,9 @@ # FIXME remove this import time -from server.hw_controller.serial_device.firmwares.grbl import Grbl -from server.hw_controller.serial_device.firmwares.marlin import Marlin -from server.hw_controller.serial_device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hardware.device.firmwares.grbl import Grbl +from server.hardware.device.firmwares.marlin import Marlin +from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler import logging if __name__ == "__main__": From 9af38804aeb03ecd550fc8c9c6dfadb8b4b881c0 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 3 Feb 2022 22:49:13 +0100 Subject: [PATCH 12/52] Started feeder --- .../hardware/device/comunication/__init__.py | 1 + .../device/comunication/device_serial.py | 2 +- .../hardware/device/estimation/cartesian.py | 5 + .../device/estimation/generic_estimator.py | 3 + server/hardware/device/estimation/polar.py | 5 + server/hardware/device/estimation/scara.py | 5 + server/hardware/device/feeder.py | 156 ++++++- .../device/firmwares/generic_firmware.py | 21 +- server/hardware/device/old/feeder.py | 415 ------------------ server/tests/test_z_firmwares.py | 2 +- test.py | 5 +- 11 files changed, 187 insertions(+), 433 deletions(-) create mode 100644 server/hardware/device/comunication/__init__.py diff --git a/server/hardware/device/comunication/__init__.py b/server/hardware/device/comunication/__init__.py new file mode 100644 index 00000000..56fe7883 --- /dev/null +++ b/server/hardware/device/comunication/__init__.py @@ -0,0 +1 @@ +# added this file to avoid linter errors/warnings diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index d13179c5..e61e4322 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -5,7 +5,7 @@ import logging import glob -from server.hardware.device.emulator import Emulator +from server.hardware.device.comunication.emulator import Emulator class DeviceSerial: diff --git a/server/hardware/device/estimation/cartesian.py b/server/hardware/device/estimation/cartesian.py index e69de29b..eeb094e3 100644 --- a/server/hardware/device/estimation/cartesian.py +++ b/server/hardware/device/estimation/cartesian.py @@ -0,0 +1,5 @@ +from server.hardware.device.estimation.generic_estimator import GenericEstimator + + +class Cartesian(GenericEstimator): + ... diff --git a/server/hardware/device/estimation/generic_estimator.py b/server/hardware/device/estimation/generic_estimator.py index 59e43c38..ea65f2e9 100644 --- a/server/hardware/device/estimation/generic_estimator.py +++ b/server/hardware/device/estimation/generic_estimator.py @@ -84,3 +84,6 @@ def parse_command(self, command): self._position.x = float(self._x_regex.findall(command)[0][0]) if "Y" in command: self._position.y = float(self._y_regex.findall(command)[0][0]) + + def __str__(self): + return f"Estimator type: {type(self).__name__}" diff --git a/server/hardware/device/estimation/polar.py b/server/hardware/device/estimation/polar.py index e69de29b..e4bebbd9 100644 --- a/server/hardware/device/estimation/polar.py +++ b/server/hardware/device/estimation/polar.py @@ -0,0 +1,5 @@ +from server.hardware.device.estimation.generic_estimator import GenericEstimator + + +class Polar(GenericEstimator): + ... diff --git a/server/hardware/device/estimation/scara.py b/server/hardware/device/estimation/scara.py index e69de29b..0ebeaef3 100644 --- a/server/hardware/device/estimation/scara.py +++ b/server/hardware/device/estimation/scara.py @@ -0,0 +1,5 @@ +from server.hardware.device.estimation.generic_estimator import GenericEstimator + + +class Scara(GenericEstimator): + ... diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index 2177640e..4c9810a1 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -1,6 +1,127 @@ -class Feeder: - def __init__(self): - pass +import logging +import os + +from dotenv import load_dotenv +from dotmap import DotMap +from threading import RLock + +from server.hardware.device.estimation.cartesian import Cartesian +from server.hardware.device.estimation.generic_estimator import GenericEstimator +from server.hardware.device.estimation.polar import Polar +from server.hardware.device.estimation.scara import Scara +from server.hardware.device.feeder_event_handler import FeederEventHandler +from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler +from server.hardware.device.firmwares.generic_firmware import GenericFirmware +from server.hardware.device.firmwares.grbl import Grbl +from server.hardware.device.firmwares.marlin import Marlin + +from server.utils import settings_utils +from server.utils.logging_utils import formatter, MultiprocessRotatingFileHandler + +# list of known firmwares +available_firmwares = DotMap({"Marlin": Marlin, "Grbl": Grbl, "Generic": GenericFirmware}) + +# list of known device types, for which an estimator has been built +available_estimators = DotMap( + {"Cartesian": Cartesian, "Polar": Polar, "Scara": Scara, "Generic": GenericEstimator} +) + + +class Feeder(FirwmareEventHandler): + """ + Feed the gcode to the device + + Handle single commands but also the preloaded scripts and complete drawings or elements + """ + + # FIXME remove the None default from the event handler + def __init__(self, event_handler: FeederEventHandler = None): + """ + Args: + event_handler: handler for the events like drawing started, drawing ended and so on + """ + # initialize logger + self.init_logger() + + self.event_handler = event_handler + self._mutex = RLock() + + self.init_device(settings_utils.load_settings()) + + def init_logger(self): + """ + Initialize the logger + + Initiate the stream logger for the command line but also the file logger for the rotating log files + """ + self.logger = logging.getLogger(__name__) + self.logger.handlers = [] # remove default handlers + self.logger.propagate = False # False -> avoid passing it to the parent logger + logging.addLevelName(settings_utils.LINE_SENT, "LINE_SENT") + logging.addLevelName(settings_utils.LINE_RECEIVED, "LINE_RECEIVED") + logging.addLevelName(settings_utils.LINE_SERVICE, "LINE_SERVICE") + + # set logger to lowest level to make availables all the levels to the handlers + # in this way can have different levels in the handlers + self.logger.setLevel(settings_utils.LINE_SERVICE) + + # create file logging handler + file_handler = MultiprocessRotatingFileHandler( + "server/logs/feeder.log", maxBytes=200000, backupCount=5 + ) + # the file logs must use the lowest level available + file_handler.setLevel(settings_utils.LINE_SERVICE) + file_handler.setFormatter(formatter) + # add handler to the logger + self.logger.addHandler(file_handler) + + # load sterr (cmd line) logging level from environment variables + load_dotenv() + level = os.getenv("FEEDER_LEVEL") + # check if the level has been set in the environment variables (should be done in the flask.env or .env files) + if not level is None: + level = int(level) + else: + level = 0 # lowest level by default + + # create stream handler (to show the log on the command line) + stream_handler = logging.StreamHandler() + stream_handler.setLevel(level) # can use a different level with respect to the file handler + stream_handler.setFormatter(formatter) + # add handler to the logger + self.logger.addHandler(stream_handler) + + # print the logger level on the command line + settings_utils.print_level(level, __name__.split(".")[-1]) + + def init_device(self, settings): + """ + Init the serial device + + Initialize the firmware depending on the settings given as argument + + Args: + settings: the settings dict + """ + self.settings = settings + firmware = settings["device"]["firmware"]["value"] + # create the device based on the choosen firmware + if not available_firmwares.has_key(firmware): + firmware = "Generic" + self._device = available_firmwares[firmware]( + settings["serial"], logger=self.logger.name, event_handler=self + ) + + # enable or disable fast mode for the device + self._device.fast_mode = settings["serial"]["fast_mode"]["value"] + + # select the right estimator depending on the device type + device_type = settings["device"]["type"]["value"] + if available_estimators.has_key(device_type): + self._device.estimator = available_estimators[device_type]() + # try to connect to the serial device + # if the device is not available will create a fake/virtual device + self._device.connect() def get_status(self): return {"is_running": True, "is_paused": True} @@ -10,3 +131,32 @@ def stop(self): def resume(self): pass + + def send_gcode_command(self, command): + with self._mutex: + self._device.send_gcode_command(command) + + def send_script(self, script): + """ + Send a series of commands (script) + + Args: + script: a string containing "\n" separated gcode commands + """ + with self._mutex: + script = script.split("\n") + for s in script: + if s != "" and s != " ": + self.send_gcode_command(s) + + # event handler methods + + def on_line_sent(self, line): + self.logger.log(settings_utils.LINE_SENT, line) + + def on_line_received(self, line): + self.logger.log(settings_utils.LINE_RECEIVED, line) + + def on_device_ready(self): + self.logger.info(f"\nDevice ready.\n{self._device}\n") + self.send_script(self.settings["scripts"]["connected"]["value"]) diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index a097c919..3dc91653 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -152,8 +152,8 @@ def connect(self): self._logger.info("Connecting to the serial device") with self.serial_mutex: self._serial_device = DeviceSerial( - self._serial_settings["serial_name"], - self._serial_settings["baudrate"], + self._serial_settings["port"]["value"], + self._serial_settings["baud"]["value"], self._logger.name, ) self._serial_device.set_on_readline_callback(self._on_readline) @@ -162,6 +162,14 @@ def connect(self): if not self._serial_device.is_connected(): self._on_device_ready() + def emergency_stop(self): + """ + Stop the device immediately + + This method must be implemented in the child class + """ + pass + def _parse_macro(self, command): """ Parse a macro @@ -309,10 +317,5 @@ def _log_received_line(self, line, hide_line=False): if not hide_line: self.event_handler.on_line_received(line) - # From here on the methods are abstract and must be implemented in the child class - - @abstractmethod - def emergency_stop(self): - """ - Stop the device immediately - """ + def __str__(self) -> str: + return f"Device:\n - firmware type: {type(self).__name__}\n - fast mode: {self.fast_mode}\n - {self.estimator}" diff --git a/server/hardware/device/old/feeder.py b/server/hardware/device/old/feeder.py index cf9d112d..c1683f7a 100644 --- a/server/hardware/device/old/feeder.py +++ b/server/hardware/device/old/feeder.py @@ -120,20 +120,6 @@ def __init__(self, handler=None, **kargvs): # device specific options self.update_settings(settings_utils.load_settings()) - def update_settings(self, settings): - self.settings = settings - self._firmware = settings["device"]["firmware"]["value"] - self._ACK = firmware.get_ACK(self._firmware) - self._timeout.set_timeout_period(firmware.get_buffer_timeout(self._firmware)) - self.is_fast_mode = settings["serial"]["fast_mode"]["value"] - if self.is_fast_mode: - if settings["device"]["type"]["value"] == "Cartesian": - self.command_resolution = "{:.1f}" # Cartesian do not need extra resolution because already using mm as units. (TODO maybe with inches can have problems? needs to check) - else: - self.command_resolution = ( - "{:.3f}" # Polar and scara use smaller numbers, will need also decimals - ) - def close(self): self.serial.close() @@ -147,32 +133,6 @@ def get_status(self): "is_paused": self._is_paused, } - def connect(self): - self.logger.info("Connecting to serial device...") - with self.serial_mutex: - if not self.serial is None: - self.serial.close() - try: - self.serial = DeviceSerial( - self.settings["serial"]["port"]["value"], - self.settings["serial"]["baud"]["value"], - logger_name=__name__, - ) - self.serial.set_onreadline_callback(self.on_serial_read) - self.serial.start_reading() - self.logger.info("Connection successfull") - except: - self.logger.info("Error during device connection") - self.logger.info(traceback.print_exc()) - self.serial = DeviceSerial(logger_name=__name__) - self.serial.set_onreadline_callback(self.on_serial_read) - self.serial.start_reading() - - self.device_ready = False # this line is set to true as soon as the board sends a message - - def set_event_handler(self, handler): - self.handler = handler - # starts to send gcode to the machine def start_element(self, element, force_stop=False): if (not force_stop) and self.is_running(): @@ -264,79 +224,6 @@ def resume(self): self._is_paused = False self.logger.info("Resumed") - # function to prepare the command to be sent. - # * command: command to send - # * hide_command=False (optional): will hide the command from being sent also to the frontend (should be used for SW control commands) - def send_gcode_command(self, command, hide_command=False): - command = self._parse_macro(command) - - if "G28" in command: - self.last_commanded_position.x = 0 - self.last_commanded_position.y = 0 - # TODO add G92 check for the positioning - - # clean the command a little - command = command.replace("\n", "").replace("\r", "").upper() - if command == " " or command == "": - return - - # some commands require to update the feeder status - # parse the command if necessary - if "M110" in command: - cs = command.split(" ") - for c in cs: - if c[0] == "N": - self.line_number = int(c[1:]) - 1 - self.command_buffer.clear() - - # check if the command is in the "BUFFERED_COMMANDS" list and stops if the buffer is full - try: - if any(code in command for code in BUFFERED_COMMANDS): - if "F" in command: - self.feedrate = float(self.feed_regex.findall(command)[0][0]) - if "X" in command: - self.last_commanded_position.x = float(self.x_regex.findall(command)[0][0]) - if "Y" in command: - self.last_commanded_position.y = float(self.y_regex.findall(command)[0][0]) - except: - self.logger.error("Cannot parse something in the command: " + command) - # wait until the lock for the buffer length is released -> means the board sent the ack for older lines and can send new ones - with self.command_send_mutex: # wait until get some "ok" command to remove extra entries from the buffer - pass - - # send the command after parsing the content - # need to use the mutex here because it is changing also the line number - with self.serial_mutex: - line = self._generate_line(command) - - self.serial.send(line) # send line - self.logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) - - # TODO fix the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident. Do not understand why it is happening - - with self.command_buffer_mutex: - 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 - - if not hide_command: - self.handler.on_new_line(line) # uses the handler callback for the new line - - if firmware.is_marlin( - self._firmware - ): # updating the command only for marlin because grbl check periodically the buffer status with the status report command - self._update_timeout() # update the timeout because a new command has been sent - - # Send a multiline script - def send_script(self, script): - self.logger.info("Sending script") - script = script.split("\n") - for s in script: - if s != "" and s != " ": - self.send_gcode_command(s) - def serial_ports_list(self): result = [] if not self.serial is None: @@ -347,35 +234,8 @@ def is_connected(self): with self.serial_mutex: return self.serial.is_connected() - # stops immediately the device - def emergency_stop(self): - self.send_gcode_command(firmware.get_emergency_stop_command(self._firmware)) - # TODO add self.close() ? - # ----- PRIVATE METHODS ----- - # prepares the board - def _on_device_ready(self): - if firmware.is_marlin(self._firmware): - self._reset_line_number() - - # grbl status report mask setup - # sandypi need to check the buffer to see if the machine has cleaned the buffer - # setup grbl to show the buffer status with the $10 command - # Grbl 1.1 https://github.com/gnea/grbl/wiki/Grbl-v1.1-Configuration - # Grbl 0.9 https://github.com/grbl/grbl/wiki/Configuring-Grbl-v0.9 - # to be compatible with both will send $10=6 (4(for v0.9) + 2(for v1.1)) - # the status will then be prompted with the "?" command when necessary - # the buffer will contain Bf:"usage of the buffer" - if firmware.is_grbl(self._firmware): - self.send_gcode_command("$10=6") - - # send the "on connection" script from the settings - self.send_script(self.settings["scripts"]["connected"]["value"]) - - # device ready event - self.handler.on_device_ready() - # run the "_on_device_ready" method with a delay def _on_device_ready_delay(self): def delay(): @@ -454,278 +314,3 @@ def on_serial_read(self, l): ]: # parse single lines if multiple \n are detected self._parse_device_line(l) self._buffered_line = str(self._buffered_line[-1]) - - def _update_timeout(self): - self._timeout_last_line = self.line_number - self._timeout.update() - - # function called when the buffer has not been updated for some time (controlled by the buffered timeou) - def _on_timeout(self): - if ( - self.command_buffer_mutex.locked - and self.line_number == self._timeout_last_line - and not self.is_paused() - ): - # self.logger.warning("!Buffer timeout. Trying to clean the buffer!") - # to clean the buffer try to send an M114 (marlin) or ? (Grbl) message. In this way will trigger the buffer cleaning mechanism - command = firmware.get_buffer_command(self._firmware) - line = self._generate_line( - command, no_buffer=True - ) # use the no_buffer to clean one position of the buffer after adding the command - self.logger.log(settings_utils.LINE_SERVICE, line) - with self.serial_mutex: - self.serial.send(line) - else: - self._update_timeout() - - def _ack_received(self, safe_line_number=None, append_left_extra=False): - if safe_line_number is None: - with self.command_buffer_mutex: - if len(self.command_buffer) != 0: - self.command_buffer.popleft() - else: - with self.command_buffer_mutex: - while True: - # Remove the numbers lower than the specified safe_line_number (used in the resend line command: lines older than the one required can be deleted safely) - if len(self.command_buffer) != 0: - line_number = self.command_buffer.popleft() - if line_number >= safe_line_number: - self.command_buffer.appendleft(line_number) - break - if append_left_extra: - self.command_buffer.appendleft(safe_line_number - 1) - - self._check_buffer_mutex_status() - - # check if the buffer of the device is full or can accept more commands - def _check_buffer_mutex_status(self): - with self.command_buffer_mutex: - if ( - self.command_send_mutex.locked() - and len(self.command_buffer) < self.command_buffer_max_length - ): - self.command_send_mutex.release() - - # parse a line coming from the device - def _parse_device_line(self, line): - # setting to avoid sending the message to the frontend in particular situations (i.e. status checking in grbl) - # will still print the status in the command line - hide_line = False - - if ( - firmware.get_ACK(self._firmware) in line - ): # when an "ack" is received free one place in the buffer - self._ack_received() - - # check if the received line is for the device being ready - if firmware.get_ready_message(self._firmware) in line: - if self.serial.is_fake: - self._on_device_ready() - else: - self._on_device_ready_delay() # if the device is ready will allow the communication after a small delay - - # check marlin specific messages - if firmware.is_grbl(self._firmware): - if line.startswith("<"): - try: - # interested in the "Bf:xx," part where xx is the content of the buffer - # select buffer content lines - res = line.split("Bf:")[1] - res = int(res.split(",")[0]) - if ( - res == 15 - ): # 15 => buffer is empty on the device (should include also 14 to make it more flexible?) - with self.command_buffer_mutex: - self.command_buffer.clear() - if res != 0: # 0 -> buffer is full - with self.command_buffer_mutex: - if len(self.command_buffer) > 0 and self.is_running(): - self.command_buffer.popleft() - self._check_buffer_mutex_status() - - if self.is_running() or self.is_paused(): - hide_line = True - self.logger.log(settings_utils.LINE_SERVICE, line) - except: # sometimes may not receive the entire line thus it may throw an error - pass - return - - # errors - elif "error:22" in line: - self.stop() - with self.command_buffer_mutex: - self.command_buffer.clear() - elif "error:" in line: - self.logger.error("Grbl error: {}".format(line)) - # TODO check/parse error types and give some hint about the problem? - - # TODO divide parser between firmwares? - # TODO set firmware type automatically on connection - # TODO add feedrate control with something like a knob on the user interface to make the drawing slower or faster - - # Marlin messages - else: - # Marlin resend command if a message is not received correctly - # Quick note: if the buffer_command is sent too often will fill the buffer with "M114" and if a line is request will not be able to send it back - # TODO Should add some sort of filter that if the requested line number is older than the requested ones can send from that number to the first an empty command or the buffer_command - # Otherwise should not put a buffer_command in the buffer and if a line with the requested number should send the buffer_command - if "Resend: " in line: - line_found = False - line_number = int(line.replace("Resend: ", "").replace("\r\n", "")) - items = deepcopy(self.command_buffer_history) - missing_lines = True - first_available_line = None - for n, c in items.items(): - n_line_number = int(n.strip("N")) - if n_line_number == line_number: - line_found = True - if n_line_number >= line_number: - if first_available_line is None: - first_available_line = line_number - # All the lines after the required one must be resent. Cannot break the loop now - self.serial.send(c) - self.logger.error( - "Line not received correctly. Resending: {}".format(c.strip("\n")) - ) - - if (not line_found) and not (first_available_line is None): - for i in range(line_number, first_available_line): - self.serial.send( - self._generate_line(firmware.MARLIN.buffer_command, no_buffer=True, n=i) - ) - - 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: - self.logger.error( - "No line was found for the number required. Restart numeration." - ) - self._reset_line_number() - - # Marlin "unknow command" - elif "echo:Unknown command:" in line: - self.logger.error("Error: command not found. Can also be a communication error") - # M114 response contains the "Count" word - # the response looks like: X:115.22 Y:116.38 Z:0.00 E:0.00 Count A:9218 B:9310 Z:0 - # still, M114 will receive the last position in the look-ahead planner thus the drawing will end first on the interface and then in the real device - elif "Count" in line: - try: - l = line.split(" ") - x = float(l[0][2:]) # remove "X:" from the string - y = float(l[1][2:]) # remove "Y:" from the string - except Exception as e: - self.logger.error("Error while parsing M114 result for line: {}".format(line)) - self.logger.exception(e) - - # if the last commanded position coincides with the current position it means the buffer on the device is empty (could happen that the position is the same between different points but the M114 command should not be that frequent to run into this problem.) TODO check if it is good enough or if should implement additional checks like a timeout - # use a tolerance instead of equality because marlin is using strange rounding for the coordinates - if ( - abs(float(self.last_commanded_position.x) - x) - < firmware.MARLIN.position_tolerance - ) and ( - abs(float(self.last_commanded_position.y) - y) - < firmware.MARLIN.position_tolerance - ): - if self.is_running(): - self._ack_received() - else: - with self.command_buffer_mutex: - self.command_buffer.clear() - self._check_buffer_mutex_status() - - if not self.is_running(): - hide_line = True - - # 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 - - self.logger.log(settings_utils.LINE_RECEIVED, line) - if not hide_line: - self.handler.on_message_received(line) - - # depending on the firmware, generates a correct line to send to the board - # args: - # * command: the gcode command to send - # * no_buffer (def: False): will not save the line in the buffer (used to get an ack to clear the buffer after a timeout if an ack is lost) - def _generate_line(self, command, no_buffer=False, n=None): - line = command - # TODO add a "fast mode" remove spaces from commands and reduce number of decimals - # removing spaces is in conflict with the emulator... Need to update the parser there also - # fast mode test - if self.is_fast_mode: - line = command.split(" ") - new_line = [] - for l in line: - if l.startswith("X"): - l = "X" + self.command_resolution.format(float(l[1:])).rstrip("0").rstrip(".") - elif l.startswith("Y"): - l = "Y" + self.command_resolution.format(float(l[1:])).rstrip("0").rstrip(".") - new_line.append(l) - line = "".join(new_line) - - # marlin needs line numbers and checksum (grbl doesn't) - if firmware.is_marlin(self._firmware): - # add line number - if ( - n is None - ): # check if the line number was specified or if must increase the number of the sequential command - self.line_number += 1 - n = self.line_number - if self.is_fast_mode: - line = "N{}{}".format(n, line) - else: - line = "N{} {} ".format(n, line) - # calculate marlin checksum according to the wiki - cs = 0 - for i in line: - cs = cs ^ ord(i) - cs &= 0xFF - - line += "*{}\n".format(cs) # add checksum to the line - - elif firmware.is_grbl(self._firmware): - if line != firmware.GRBL.buffer_command: - line += "\n" - - else: - line += "\n" - - # store the line in the buffer - with self.command_buffer_mutex: - self.command_buffer.append(self.line_number) - self.command_buffer_history["N{}".format(self.line_number)] = line - if no_buffer: - self.command_buffer.popleft() # remove an element to get a free ack from the non buffered command. Still must keep it in the buffer in the case of an error in sending the line - - return line - - def _reset_line_number(self, line_number=2): - # Marlin may require to reset the line numbers - if firmware.is_marlin(self._firmware): - self.logger.info("Resetting line number") - self.send_gcode_command("M110 N{}".format(line_number)) - # Grbl do not use line numbers - - def _parse_macro(self, command): - if not MACRO_CHAR in command: - return command - macros = self.macro_regex.findall(command) - for m in macros: - try: - # see https://pypi.org/project/py-expression-eval/ for more info about the parser - res = self.macro_parser.parse(m).evaluate( - { - "X": self.last_commanded_position.x, - "x": self.last_commanded_position.x, - "Y": self.last_commanded_position.y, - "y": self.last_commanded_position.y, - "F": self.feedrate, - "f": self.feedrate, - } - ) - command = command.replace(MACRO_CHAR + m + MACRO_CHAR, str(res)) - except Exception as e: - self.logger.error("Error while parsing macro: " + m) - self.logger.error(e) - return command diff --git a/server/tests/test_z_firmwares.py b/server/tests/test_z_firmwares.py index 29cc5457..a61c70a6 100644 --- a/server/tests/test_z_firmwares.py +++ b/server/tests/test_z_firmwares.py @@ -20,7 +20,7 @@ def on_device_ready(self): print("Device ready") -settings = {"serial_name": "COM3", "baudrate": 115200} +settings = {"port": {"value": "COM3"}, "baud": {"value": 115200}} logger_name = logging.getLogger().name diff --git a/test.py b/test.py index ac0418a7..47ace833 100644 --- a/test.py +++ b/test.py @@ -14,9 +14,6 @@ def test_device(device): device.send_gcode_command("G0 X0 Y0 F300") for x in range(15): device.send_gcode_command(f"G0 X{x*2} Y0") - print("Done") - # time.sleep(6) - assert len(device.buffer) == 0 class EventHandler(FirwmareEventHandler): def on_line_received(self, line): @@ -28,7 +25,7 @@ def on_line_sent(self, line): def on_device_ready(self): print("Device ready") - settings = {"serial_name": "COM3", "baudrate": 115200} + settings = {"port": {"value": "COM3"}, "baud": {"value": 115200}} print("Testing Marlin") logger_name = logging.getLogger().name From ad930803dae804c91c12c956cef4f2c2334b65e8 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 6 Feb 2022 14:50:23 +0100 Subject: [PATCH 13/52] Finished the feeder refactoring. --- docs/old_troubleshooting.md | 2 +- .../tabs/settings/defaultSettings.js | 4 +- server/__init__.py | 12 +- .../device/comunication/device_serial.py | 12 +- server/hardware/device/feeder.py | 214 +++++++++++- .../device/firmwares/generic_firmware.py | 41 ++- server/hardware/device/firmwares/marlin.py | 10 + server/hardware/device/old/feeder.py | 316 ------------------ .../hardware/device/old/firmware_defaults.py | 51 --- server/hardware/feeder_event_manager.py | 5 +- server/hardware/queue_manager.py | 8 +- server/saves/default_settings.json | 51 ++- .../sockets_interface/socketio_callbacks.py | 12 +- server/utils/settings_utils.py | 15 - 14 files changed, 313 insertions(+), 440 deletions(-) delete mode 100644 server/hardware/device/old/feeder.py delete mode 100644 server/hardware/device/old/firmware_defaults.py diff --git a/docs/old_troubleshooting.md b/docs/old_troubleshooting.md index 308c0c95..93993452 100644 --- a/docs/old_troubleshooting.md +++ b/docs/old_troubleshooting.md @@ -11,7 +11,7 @@ $> source env/bin/activate (env) $> python3 -m pip install pyserial ``` -## "Serial not available. Will use fake device" +## "Serial not available. Will use virtual device" The previous message may appear on the command line while running the program. This is a normal behaviour on the first run because it is necessary to select the serial device to connect from the UI. diff --git a/frontend/src/structure/tabs/settings/defaultSettings.js b/frontend/src/structure/tabs/settings/defaultSettings.js index 613c723e..fd891f1c 100644 --- a/frontend/src/structure/tabs/settings/defaultSettings.js +++ b/frontend/src/structure/tabs/settings/defaultSettings.js @@ -6,10 +6,10 @@ const defaultSettings = { port: { name: "serial.port", type: "select", - value: "FAKE", + value: "Virtual", label: "Serial port", available_values: [ - "FAKE" + "Virtual" ], tip: "Select the serial port" }, diff --git a/server/__init__.py b/server/__init__.py index 198f2dcd..476d95b6 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -1,4 +1,3 @@ -from server.utils.settings_utils import get_ip4_addresses from flask import Flask, url_for from flask.helpers import send_from_directory from flask_socketio import SocketIO @@ -23,14 +22,6 @@ # Updating setting files (will apply changes only when a new SW version is installed) settings_utils.update_settings_file_version() -# Shows ipv4 adresses -print( - "\nTo run the server use 'ip:5000' in your browser with one of the following ip adresses: {}\n".format( - str(get_ip4_addresses()) - ), - flush=True, -) - # Logging setup load_dotenv() level = os.getenv("FLASK_LEVEL") @@ -99,6 +90,7 @@ def base_static(filename): import server.api.drawings from server.sockets_interface.socketio_emits import SocketioEmits import server.sockets_interface.socketio_callbacks + from server.hardware.feeder_event_manager import FeederEventManager from server.hardware.device.feeder import Feeder from server.hardware.queue_manager import QueueManager from server.preprocessing.file_observer import GcodeObserverManager @@ -113,7 +105,7 @@ def base_static(filename): app.semits = SocketioEmits(app, socketio, db) # Device controller initialization -app.feeder = Feeder() +app.feeder = Feeder(FeederEventManager(app)) app.qmanager = QueueManager(app, socketio) # Buttons controller initialization diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index e61e4322..63fdcd0e 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -26,7 +26,7 @@ def __init__(self, serial_name=None, baudrate=115200, logger_name=None): ) self.serialname = serial_name self.baudrate = baudrate - self.is_fake = False + self.is_virtual = False self._buffer = bytearray() self.echo = "" self._emulator = Emulator() @@ -57,9 +57,9 @@ def open(self): except Exception as e: # FIXME should check for different exceptions self.logger.exception(e) - self.is_fake = True + self.is_virtual = True self.logger.error( - "Serial not available. Are you sure the device is connected and is not in use by other softwares? (Will use the fake serial)" + "Serial not available. Are you sure the device is connected and is not in use by other softwares? (Will use the virtual serial)" ) self._th.start() @@ -95,7 +95,7 @@ def send(self, line): Args: line: the line to send to the device """ - if self.is_fake: + if self.is_virtual: self._emulator.send(line) else: if self.serial.is_open: @@ -115,7 +115,7 @@ def is_connected(self): Returns: True if the serial is open on a real device """ - if self.is_fake: + if self.is_virtual: return False return self.serial.is_open @@ -136,7 +136,7 @@ def _readline(self): """ Reads a line from the device (if available) and call the callback """ - if not self.is_fake: + if not self.is_virtual: if self.serial.is_open: while self.serial.in_waiting > 0: line = self.serial.readline() diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index 4c9810a1..7b7d1c8f 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -1,9 +1,11 @@ import logging import os +import time +from threading import RLock, Thread from dotenv import load_dotenv from dotmap import DotMap -from threading import RLock +from server.database.playlist_elements import DrawingElement, TimeElement from server.hardware.device.estimation.cartesian import Cartesian from server.hardware.device.estimation.generic_estimator import GenericEstimator @@ -15,6 +17,8 @@ from server.hardware.device.firmwares.grbl import Grbl from server.hardware.device.firmwares.marlin import Marlin +from server.database.generic_playlist_element import UNKNOWN_PROGRESS, GenericPlaylistElement + from server.utils import settings_utils from server.utils.logging_utils import formatter, MultiprocessRotatingFileHandler @@ -35,7 +39,7 @@ class Feeder(FirwmareEventHandler): """ # FIXME remove the None default from the event handler - def __init__(self, event_handler: FeederEventHandler = None): + def __init__(self, event_handler: FeederEventHandler): """ Args: event_handler: handler for the events like drawing started, drawing ended and so on @@ -45,7 +49,17 @@ def __init__(self, event_handler: FeederEventHandler = None): self.event_handler = event_handler self._mutex = RLock() + self._device = None + + # feeder variables + self._status = DotMap({"running": False, "paused": False, "progress": UNKNOWN_PROGRESS}) + self._current_element = None + # self._stopped will be true when the device is correctly stopped after calling stop() + self._stopped = False + # thread instance running the elements + self.__th = None + # initialize the device self.init_device(settings_utils.load_settings()) def init_logger(self): @@ -103,6 +117,11 @@ def init_device(self, settings): Args: settings: the settings dict """ + # close connection with previous settings if available + if not self._device is None: + if self._device.is_connected(): + self._device.close() + self.settings = settings firmware = settings["device"]["firmware"]["value"] # create the device based on the choosen firmware @@ -123,18 +142,97 @@ def init_device(self, settings): # if the device is not available will create a fake/virtual device self._device.connect() - def get_status(self): - return {"is_running": True, "is_paused": True} + def is_connected(self): + """ + Returns: + True if is connected to a real device + False if is using a virtual device + """ + with self._mutex: + return self._device.is_connected() - def stop(self): - pass + @property + def status(self): + """ + Returns: + dict with the current status: + * running: True if there is a drawing going on + * paused: if the device is paused + * progress: the progress of the current element + """ + with self._mutex: + self._status.progress = ( + self._current_element.get_progress(1000) + if not self._current_element is None + else UNKNOWN_PROGRESS + ) + # FIXME use feedrate in get_progress argument!! + return self._status + + @property + def current_element(self): + """ + Returns: + currently being used element + """ + with self._mutex: + return self._current_element + + def pause(self): + """ + Pause the current drawing + """ + with self._mutex: + self._status.paused = True + self.logger.info("Paused") def resume(self): - pass + """ + Resume the current paused drawing + """ + with self._mutex: + self._status.paused = False + self.logger.info("Resumed") - def send_gcode_command(self, command): + def stop(self): + """ + Stop the current element + + This is a blocking function. Will wait until the element is completely stopped before going on with the execution + """ + # TODO: make it non blocking since the even is called when the drawing is stopped with self._mutex: - self._device.send_gcode_command(command) + # if is not running, no need to stop it + if not self._status.running: + return + + tmp = ( + self._current_element + ) # store the current element to raise the "on_element_ended" callback + self._current_element = None + self._status.running = False + if not self._stopped: + self.logger.info("Stopping drawing") + while True: + if self._stopped: + break + + # waiting comand buffer to be cleared before calling the "drawing ended" event + while True: + if len(self._device.buffer) == 0: + break + + # clean the device status + self._device.reset_status() + + # call the element ended callback + self.event_handler.on_element_ended(tmp) + + def send_gcode_command(self, command): + """ + Send a gcode command to the device + """ + self._device.send_gcode_command(command) def send_script(self, script): """ @@ -149,14 +247,112 @@ def send_script(self, script): if s != "" and s != " ": self.send_gcode_command(s) + def start_element(self, element: GenericPlaylistElement): + """ + Start the given element + + The element will start only if the feeder is not running. + If there is already something running will not run the element and return False. + To run an element must first stop the feeder. The "on_element_ended" callback will be raised when the device is stopped + + Args: + element: the element to be played + + Returns: + True if the element is being started, False otherwise + """ + with self._mutex: + # if is already running something and the force_stop is not set will return False directly + if self._status.running: + return False + + # starting the thread + self.__th = Thread(target=self.__thf, daemon=True) + self.__th.name = "feeder_send_element" + + # resetting status + self._status.running = True + self._status.paused = False + self._stopped = False + self._current_element = element + self._device.buffer.clear() + # starting the thread + self.__th.start() + + # callback for the element being started + self.event_handler.on_element_started(element) + + return True + + def update_current_time_element(self, new_interval): + """ + If the current element is a TimeElement, allow to change the interval value to update the due date + + Args: + new_interval: the new interval value for the TimeElement + """ + with self._mutex: + if type(self._current_element) is TimeElement: + if self._current_element.type == "delay": + self._current_element.update_delay(new_interval) + # event handler methods def on_line_sent(self, line): self.logger.log(settings_utils.LINE_SENT, line) + self.event_handler.on_new_line(line) def on_line_received(self, line): self.logger.log(settings_utils.LINE_RECEIVED, line) + self.event_handler.on_message_received(line) def on_device_ready(self): self.logger.info(f"\nDevice ready.\n{self._device}\n") self.send_script(self.settings["scripts"]["connected"]["value"]) + + # private methods + + def __thf(self): + """ + This function handle the element once the start element method is called + + This function must not be called directly but will run in a separate thread + """ + # run the "before" script only if the given element is a drawing + with self._mutex: + if isinstance(self._current_element, DrawingElement): + self.send_script(self.settings["scripts"]["before"]["value"]) + + self.logger.info(f"Starting new drawing with code {self._current_element}") + + # TODO add "scale/fit/clip" filters + + # execute the command (iterate over the lines/commands or just execute what is necessary) + for k, line in enumerate(self._current_element.execute(self.logger)): + self.logger.info("Test2") + # if the feeder is being stopped the running flag will be False -> should exit the loop immediately + with self._mutex: + if not self._status.running: + break + + # if the line is None should just go to the next iteration + if line is None: + continue + self.send_gcode_command(line) # send the line to the device + + # if the feeder is paused should just wait until the drawing is resumed + while True: + with self._mutex: + # if not paused or if a stop command is used should exit the loop + if not self._status.paused or not self._status.running: + break + time.sleep(0.1) + + # run the "after" script only if the given element is a drawing + with self._mutex: + if isinstance(self._current_element, DrawingElement): + self.send_script(self.settings["scripts"]["after"]["value"]) + + self._stopped = True + if self._status.running: + self.stop() diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index 3dc91653..22df62e8 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -1,8 +1,6 @@ -from collections import deque import logging import re -from abc import ABC, abstractmethod from threading import RLock, Lock from py_expression_eval import Parser from server.hardware.device.comunication.device_serial import DeviceSerial @@ -19,7 +17,7 @@ BUFFERED_COMMANDS = ("G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03", "G28", "G92") -class GenericFirmware(ABC): +class GenericFirmware: """ Abstract class for a firmware @@ -53,7 +51,7 @@ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler) self.estimator = GenericEstimator() # buffer control - self.buffer = CommandBuffer(8) + self._buffer = CommandBuffer(8) # timeout setup self.force_ack_command = "" # command used to force an ack @@ -106,6 +104,15 @@ def feedrate(self, feedrate): with self._mutex: self._feedrate = feedrate + @property + def buffer(self): + """ + Returns: + the buffer of commands sent to the device + """ + with self._mutex: + return self._buffer + def is_ready(self): """ Returns: @@ -137,7 +144,7 @@ def send_gcode_command(self, command, hide_command=False): command = self._prepare_command(command) # wait until the lock for the buffer length is released # if the lock is released means the board sent the ack for older lines and can send new ones - with self.buffer.get_buffer_wait_mutex(): + with self._buffer.get_buffer_wait_mutex(): pass with self._mutex: self._handle_send_command(command, hide_command) @@ -162,6 +169,13 @@ def connect(self): if not self._serial_device.is_connected(): self._on_device_ready() + def close(self): + """ + Close the comunication with the device + """ + with self._mutex: + self._serial_device.close() + def emergency_stop(self): """ Stop the device immediately @@ -170,6 +184,15 @@ def emergency_stop(self): """ pass + def reset_status(self): + """ + Method that should be called when a job is stopped or finished + + Allow to reset the current status or variables and have a clean start for the next job + """ + with self._mutex: + self.line_number = 0 + def _parse_macro(self, command): """ Parse a macro @@ -220,7 +243,7 @@ def _handle_send_command(self, command, hide_command=False): line = self._generate_line(command) self._serial_device.send(line) # send line - self.buffer.push_command(line, self.line_number) + self._buffer.push_command(line, self.line_number) self._logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) # TODO fix the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident. Do not understand why it is happening @@ -261,7 +284,7 @@ def _on_timeout(self): """ with self._mutex: if ( - self.buffer.get_buffer_wait_mutex().locked + self._buffer.get_buffer_wait_mutex().locked and self.line_number == self._timeout_last_line ): # self.logger.warning("!Buffer timeout. Trying to clean the buffer!") @@ -292,13 +315,13 @@ def _on_readline(self, line): if line is None: return False if self.ack in line: - self.buffer.ack_received() + self._buffer.ack_received() return True def _on_device_ready(self): print("test") """ - Called when the connected device is ready to receive commands + Called when the connected device is ready to receive commands """ with self._mutex: self.event_handler.on_device_ready() diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index df70e4de..eb8888a4 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -25,6 +25,16 @@ def emergency_stop(self): """ self.send_gcode_command("M112") + def reset_status(self): + """ + To be called when a job is stopped/finished + + With Marlin, will also reset the line number + + """ + super().reset_status() + self._reset_line_number() + def _on_readline(self, line): """ Parse the line received from the device diff --git a/server/hardware/device/old/feeder.py b/server/hardware/device/old/feeder.py deleted file mode 100644 index c1683f7a..00000000 --- a/server/hardware/device/old/feeder.py +++ /dev/null @@ -1,316 +0,0 @@ -from threading import Thread, Lock -import os -import time -import traceback -from collections import deque -from copy import deepcopy -import re -import logging -from dotenv import load_dotenv -from dotmap import DotMap -from py_expression_eval import Parser - -from server.utils import limited_size_dict, buffered_timeout, settings_utils -from server.utils.logging_utils import formatter, MultiprocessRotatingFileHandler -from server.hardware.device.feeder_event_handler import FeederEventHandler -from server.hardware.device.device_serial import DeviceSerial -from server.hardware.device.gcode_rescalers import Fit -import server.hardware.device.firmware_defaults as firmware -from server.database.playlist_elements import DrawingElement, TimeElement -from server.database.generic_playlist_element import UNKNOWN_PROGRESS - -""" - -This class duty is to send commands to the hw. It can handle single commands as well as elements. - - -""" - -# List of commands that are buffered by the controller -BUFFERED_COMMANDS = ("G0", "G00", "G1", "G01", "G2", "G02", "G3", "G03", "G28") -# Defines the character used to define macros -MACRO_CHAR = "&" - - -class Feeder: - def __init__(self, handler=None, **kargvs): - - # logger setup - self.logger = logging.getLogger(__name__) - self.logger.handlers = [] # remove all handlers - self.logger.propagate = False # set it to False to avoid passing it to the parent logger - # add custom logging levels - logging.addLevelName(settings_utils.LINE_SENT, "LINE_SENT") - logging.addLevelName(settings_utils.LINE_RECEIVED, "LINE_RECEIVED") - logging.addLevelName(settings_utils.LINE_SERVICE, "LINE_SERVICE") - self.logger.setLevel(settings_utils.LINE_SERVICE) # set to logger lowest level - - # create file logging handler - file_handler = MultiprocessRotatingFileHandler( - "server/logs/feeder.log", maxBytes=200000, backupCount=5 - ) - file_handler.setLevel(settings_utils.LINE_SERVICE) - file_handler.setFormatter(formatter) - self.logger.addHandler(file_handler) - - # load sterr logging level from environment variables - load_dotenv() - level = os.getenv("FEEDER_LEVEL") - if not level is None: - level = int(level) - else: - level = 0 - - # create stream handler - stream_handler = logging.StreamHandler() - stream_handler.setLevel(level) - stream_handler.setFormatter(formatter) - self.logger.addHandler(stream_handler) - - settings_utils.print_level(level, __name__.split(".")[-1]) - - # variables setup - - self._current_element = None - self._is_running = False - self._stopped = False - self._is_paused = False - self._th = None - self.serial_mutex = Lock() - self.status_mutex = Lock() - if handler is None: - self.handler = FeederEventHandler() - else: - self.handler = handler - self.serial = DeviceSerial(logger_name=__name__) - self.line_number = 0 - self._timeout_last_line = self.line_number - self.feedrate = 0 - self.last_commanded_position = DotMap({"x": 0, "y": 0}) - - # commands parser - self.feed_regex = re.compile( - "[F]([0-9.-]+)($|\s)" - ) # looks for a +/- float number after an F, until the first space or the end of the line - self.x_regex = re.compile( - "[X]([0-9.-]+)($|\s)" - ) # looks for a +/- float number after an X, until the first space or the end of the line - self.y_regex = re.compile( - "[Y]([0-9.-]+)($|\s)" - ) # looks for a +/- float number after an Y, until the first space or the end of the line - self.macro_regex = re.compile( - MACRO_CHAR + "(.*?)" + MACRO_CHAR - ) # looks for stuff between two "%" symbols. Used to parse macros - - self.macro_parser = Parser() # macro expressions parser - - # buffer controll 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_max_length = 8 - self.command_buffer_history = limited_size_dict.LimitedSizeDict( - size_limit=self.command_buffer_max_length + 40 - ) # keep saved the last n commands - self._buffered_line = "" - - self._timeout = buffered_timeout.BufferTimeout(30, self._on_timeout) - self._timeout.start() - - # device specific options - self.update_settings(settings_utils.load_settings()) - - def close(self): - self.serial.close() - - def get_status(self): - with self.status_mutex: - return { - "is_running": self._is_running, - "progress": self._current_element.get_progress(self.feedrate) - if not self._current_element is None - else UNKNOWN_PROGRESS, - "is_paused": self._is_paused, - } - - # starts to send gcode to the machine - def start_element(self, element, force_stop=False): - if (not force_stop) and self.is_running(): - return False # if a file is already being sent it will not start a new one - else: - if self.is_running(): - self.stop() # stop -> blocking function: wait until the thread is stopped for real - with self.serial_mutex: - self._th = Thread(target=self._thf, args=(element,), daemon=True) - self._th.name = "drawing_feeder" - self._is_running = True - self._stopped = False - self._is_paused = False - self._current_element = element - if self.command_send_mutex.locked(): - self.command_send_mutex.release() - with self.command_buffer_mutex: - self.command_buffer.clear() - self._th.start() - self.handler.on_element_started(element) - - # ask if the feeder is already sending a file - def is_running(self): - with self.status_mutex: - return self._is_running - - # ask if the feeder is paused - def is_paused(self): - with self.status_mutex: - return self._is_paused - - # return the code of the drawing on the go - def get_element(self): - with self.status_mutex: - return self._current_element - - def update_current_time_element(self, new_interval): - with self.status_mutex: - if type(self._current_element) is TimeElement: - if self._current_element.type == "delay": - self._current_element.update_delay(new_interval) - - # stops the drawing - # blocking function: waits until the thread is stopped - def stop(self): - if self.is_running(): - tmp = self._current_element - with self.status_mutex: - if not self._stopped: - self.logger.info("Stopping drawing") - self._is_running = False - self._current_element = None - # block the function until the thread is stopped otherwise the thread may still be running when the new thread is started - # (_isrunning will turn True and the old thread will keep going) - while True: - with self.status_mutex: - if self._stopped: - break - - # waiting command buffer to be clear before calling the "drawing ended" event - while True: - self.send_gcode_command( - firmware.get_buffer_command(self._firmware), hide_command=True - ) - time.sleep( - 3 - ) # wait 3 second to get the time to the board to answer. If the time here is reduced too much will fill the buffer history with buffer_commands and may loose the needed line in a resend command for marlin - # the "buffer_command" will raise a response from the board that will be handled by the parser to empty the buffer - - # wait until the buffer is empty to know that the job is done - with self.command_buffer_mutex: - if len(self.command_buffer) == 0: - break - # resetting line number between drawings - self._reset_line_number() - # calling "drawing ended" event - self.handler.on_element_ended(tmp) - - # pauses the drawing - # can resume with "resume()" - def pause(self): - with self.status_mutex: - self._is_paused = True - self.logger.info("Paused") - - # resumes the drawing (only if used with "pause()" and not "stop()") - def resume(self): - with self.status_mutex: - self._is_paused = False - self.logger.info("Resumed") - - def serial_ports_list(self): - result = [] - if not self.serial is None: - result = self.serial.serial_port_list() - return result - - def is_connected(self): - with self.serial_mutex: - return self.serial.is_connected() - - # ----- PRIVATE METHODS ----- - - # run the "_on_device_ready" method with a delay - def _on_device_ready_delay(self): - def delay(): - time.sleep(5) - self._on_device_ready() - - th = Thread(target=delay, daemon=True) - th.name = "waiting_device_ready" - th.start() - - # thread function - # TODO move this function in a different class? - def _thf(self, element): - # runs the script only it the element is a drawing, otherwise will skip the "before" script - if isinstance(element, DrawingElement): - self.send_script(self.settings["scripts"]["before"]["value"]) - - self.logger.info("Starting new drawing with code {}".format(element)) - - # 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, - } - - filter = Fit(dims) - - for k, line in enumerate( - self.get_element().execute(self.logger) - ): # execute the element (iterate over the commands or do what the element is designed for) - if not self.is_running(): - break - - if ( - line is None - ): # if the line is none there is no command to send, will continue with the next element execution (for example, within the delay element it will sleep 1s at a time and return None until the timeout passed. TODO Not really an efficient way, may change it in the future) - continue - - line = line.upper() - - self.send_gcode_command(line) - - while self.is_paused(): - time.sleep(0.1) - # if a "stop" command is raised must exit the pause and stop the drawing - if not self.is_running(): - break - - # TODO parse line to scale/add padding to the drawing according to the drawing settings (in order to keep the original .gcode file) - # line = filter.parse_line(line) - # line = "N{} ".format(file_line) + line - with self.status_mutex: - self._stopped = True - - # runs the script only it the element is a drawing, otherwise will skip the "after" script - if isinstance(element, DrawingElement): - self.send_script(self.settings["scripts"]["after"]["value"]) - if self.is_running(): - self.stop() - - # thread that keep reading the serial port - def on_serial_read(self, l): - if not l is None: - # readline is not returning the full line but only a buffer - # must break the line on "\n" to correctly parse the result - self._buffered_line += l - if "\n" in self._buffered_line: - self._buffered_line = self._buffered_line.replace("\r", "").split("\n") - if len(self._buffered_line) > 1: - for l in self._buffered_line[ - 0:-1 - ]: # parse single lines if multiple \n are detected - self._parse_device_line(l) - self._buffered_line = str(self._buffered_line[-1]) diff --git a/server/hardware/device/old/firmware_defaults.py b/server/hardware/device/old/firmware_defaults.py deleted file mode 100644 index 67e31ac2..00000000 --- a/server/hardware/device/old/firmware_defaults.py +++ /dev/null @@ -1,51 +0,0 @@ -from dotmap import DotMap - -MARLIN = DotMap() -MARLIN.name = "Marlin" -MARLIN.ACK = "ok" -MARLIN.buffer_command = "M114" -MARLIN.emergency_stop = "M112" -MARLIN.buffer_timeout = 30 -MARLIN.ready_message = "start" -MARLIN.position_tolerance = 0.01 - -def is_marlin(val): - return val == MARLIN.name - - -GRBL = DotMap() -GRBL.name = "Grbl" -GRBL.ACK = "ok" -GRBL.buffer_command = "?" -GRBL.emergency_stop = "!" -GRBL.buffer_timeout = 5 -GRBL.ready_message = "Grbl" - -def is_grbl(val): - return val == GRBL.name - - -def get_ACK(firmware): - if firmware == MARLIN.name: - return MARLIN.ACK - else: return GRBL.ACK - -def get_buffer_command(firmware): - if firmware == MARLIN.name: - return MARLIN.buffer_command - else: return GRBL.buffer_command - -def get_buffer_timeout(firmware): - if firmware == MARLIN.name: - return MARLIN.buffer_timeout - else: return GRBL.buffer_timeout - -def get_emergency_stop_command(firmware): - if firmware == MARLIN.name: - return MARLIN.emergency_stop - else: return GRBL.emergency_stop - -def get_ready_message(firmware): - if firmware == MARLIN.name: - return MARLIN.ready_message - else: return GRBL.ready_message \ No newline at end of file diff --git a/server/hardware/feeder_event_manager.py b/server/hardware/feeder_event_manager.py index e9c915c4..cdc1e447 100644 --- a/server/hardware/feeder_event_manager.py +++ b/server/hardware/feeder_event_manager.py @@ -1,9 +1,12 @@ -from server.database.playlist_elements import DrawingElement from server.hardware.device.feeder_event_handler import FeederEventHandler import time class FeederEventManager(FeederEventHandler): + """ + Handle the events from the feeder + """ + def __init__(self, app): super().__init__() self.app = app diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index d91e16eb..9947e579 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -198,7 +198,7 @@ def is_paused(self): True if the device is paused """ with self._mutex: - return self.app.feeder.get_status()["is_paused"] + return self.app.feeder.status.paused def is_drawing(self): """ @@ -208,7 +208,7 @@ def is_drawing(self): True if there is a drawing running in the feeder """ with self._mutex: - return self.app.feeder.get_status()["is_running"] + return self.app.feeder.status.running def pause(self): """ @@ -432,7 +432,7 @@ def send_queue_status(self): res = { "current_element": str(self._element), "elements": elements, - "status": self.app.feeder.get_status(), + "status": self.app.feeder.status, "repeat": self.repeat, "shuffle": self.shuffle, "interval": self.interval, @@ -448,7 +448,7 @@ def _start_element(self, element): element = element.before_start(self.app) if not element is None: self.app.logger.info("Sending gcode start command") - self.app.feeder.start_element(element, force_stop=True) + self.app.feeder.start_element(element) else: self.start_next() diff --git a/server/saves/default_settings.json b/server/saves/default_settings.json index 3c74f6c2..0bfd2b0c 100644 --- a/server/saves/default_settings.json +++ b/server/saves/default_settings.json @@ -3,9 +3,11 @@ "port": { "name": "serial.port", "type": "select", - "value": "FAKE", + "value": "Virtual", "label": "Serial port", - "available_values": ["FAKE"], + "available_values": [ + "Virtual" + ], "tip": "Select the serial port" }, "baud": { @@ -13,7 +15,19 @@ "type": "select", "value": "115200", "label": "Serial baudrate", - "available_values": ["2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "250000", "460800", "921600"], + "available_values": [ + "2400", + "4800", + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "250000", + "460800", + "921600" + ], "tip": "Select the correct serial baudrate" }, "fast_mode": { @@ -48,22 +62,26 @@ "label": "Select device type", "tip": "Select the type of mechanism used by the device" }, - "width":{ + "width": { "name": "device.width", "type": "input", "value": 100, "label": "Device width", "depends_on": "device.type", - "depends_values": ["Cartesian"], + "depends_values": [ + "Cartesian" + ], "tip": "Maximum X extension" }, - "height":{ + "height": { "name": "device.height", "type": "input", "value": 100, "label": "Device height", "depends_on": "device.type", - "depends_values": ["Cartesian"], + "depends_values": [ + "Cartesian" + ], "tip": "Maximum Y extension" }, "radius": { @@ -72,7 +90,10 @@ "value": 200, "label": "Device radius", "depends_on": "device.type", - "depends_values": ["Polar", "Scara"], + "depends_values": [ + "Polar", + "Scara" + ], "tip": "Device maximum radius" }, "angle_conversion_factor": { @@ -81,7 +102,10 @@ "value": 6, "label": "Angle conversion factor", "depends_on": "device.type", - "depends_values": ["Polar", "Scara"], + "depends_values": [ + "Polar", + "Scara" + ], "tip": "The value that makes the arm to turn one full turn" }, "offset_angle_1": { @@ -90,7 +114,10 @@ "value": -1.5, "label": "Insert angular position homing offset", "depends_on": "device.type", - "depends_values": ["Polar", "Scara"], + "depends_values": [ + "Polar", + "Scara" + ], "tip": "Angle for the home position of the arm (uses the values from the conversion factor, not rad: if angle_conversion_factor is 6 and must shift the homing by half turn must put 1.5" }, "offset_angle_2": { @@ -99,7 +126,9 @@ "value": 1.5, "label": "Insert second arm homing position offset", "depends_on": "device.type", - "depends_values": ["Scara"], + "depends_values": [ + "Scara" + ], "tip": "Angle for the home position of the second arm (uses the values from the conversion factor, not rad: if angle_conversion_factor is 6 and must shift the homing by half turn must put 1.5" } }, diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index 96d1577a..61c4db2b 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -1,4 +1,5 @@ # pylint: disable=E1101 +# pylint: disable=missing-function-docstring import json import shutil @@ -6,10 +7,11 @@ from server import socketio, app, db -from server.utils import settings_utils from server.database.elements_factory import ElementsFactory from server.database.models import UploadedFiles, Playlists from server.database.playlist_elements import DrawingElement +from server.hardware.device.comunication.device_serial import DeviceSerial +from server.utils import settings_utils @socketio.on("connect") @@ -96,7 +98,7 @@ def playlist_refresh_single(playlist_id): def settings_save(data, is_connect): settings_utils.save_settings(data) settings = settings_utils.load_settings() - app.feeder.update_settings(settings) + app.feeder.init_device(settings) app.bmanager.update_settings(settings) app.lmanager.update_settings(settings) app.semits.show_toast_on_UI("Settings saved") @@ -109,7 +111,7 @@ def settings_save(data, is_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.") + app.semits.show_toast_on_UI("Device not connected. Opening a virtual serial port.") @socketio.on("settings_request") @@ -125,8 +127,8 @@ def settings_request(): settings["leds"]["has_light_sensor"]["value"] = app.lmanager.has_light_sensor() or int( os.getenv("DEV_HWLEDS", default="0") ) - settings["serial"]["port"]["available_values"] = app.feeder.serial_ports_list() - settings["serial"]["port"]["available_values"].append("FAKE") + settings["serial"]["port"]["available_values"] = DeviceSerial.get_serial_port_list() + settings["serial"]["port"]["available_values"].append("Virtual") settings["updates"]["hash"] = app.umanager.short_hash settings["updates"][ "docker_compose_latest_version" diff --git a/server/utils/settings_utils.py b/server/utils/settings_utils.py index 778b1f54..947e5fd1 100644 --- a/server/utils/settings_utils.py +++ b/server/utils/settings_utils.py @@ -3,7 +3,6 @@ import json import logging import platform -from netifaces import interfaces, ifaddresses, AF_INET # Logging levels (see the documentation of the logging module for more details) LINE_SENT = 6 @@ -108,19 +107,6 @@ def print_level(level, logger_name): print("Logger '{}' level: {} ({})".format(logger_name, level, description)) -def get_ip4_addresses(): - ip_list = [] - for interface in interfaces(): - try: - for link in ifaddresses(interface)[AF_INET]: - ip_list.append(link["addr"]) - except: - # if the interface is whitout ipv4 adresses can just pass - pass - - return ip_list - - # To run it must be in "$(env) server>" and use "python utils/settings_utils.py" if __name__ == "__main__": # testing update_settings_file_version @@ -136,7 +122,6 @@ def get_ip4_addresses(): print(c == {"a": 0, "b": {"c": 2, "d": 4, "e": 5}, "d": 5, "c": 3}) update_settings_file_version() - print(get_ip4_addresses()) d = {"a": 500, "b": {"asf": 3, "value": 10}, "c": {"d": {"fds": 29, "value": 32}}} print(get_only_values(d)) From a67403ca6f7cbabb6c6b3dd11de982297a07bd39 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 20 Feb 2022 12:18:31 +0100 Subject: [PATCH 14/52] Bug fixes for queue and controls with virtual device. Still need to try with a real device --- .../tabs/playlists/Playlists.slice.js | 47 ++++++++++--------- .../src/structure/tabs/queue/Queue.slice.js | 28 +++++------ frontend/src/structure/tabs/queue/selector.js | 34 +++++++------- server/__init__.py | 2 +- server/database/models.py | 2 +- .../hardware/device/comunication/emulator.py | 2 + .../device/estimation/generic_estimator.py | 6 ++- server/hardware/device/feeder.py | 11 +++-- server/hardware/queue_manager.py | 5 +- .../sockets_interface/socketio_callbacks.py | 2 +- 10 files changed, 75 insertions(+), 64 deletions(-) diff --git a/frontend/src/structure/tabs/playlists/Playlists.slice.js b/frontend/src/structure/tabs/playlists/Playlists.slice.js index 63d8f16b..b5c43756 100644 --- a/frontend/src/structure/tabs/playlists/Playlists.slice.js +++ b/frontend/src/structure/tabs/playlists/Playlists.slice.js @@ -17,13 +17,13 @@ const playlistsSlice = createSlice({ let elements = action.payload.elements; const playlistId = action.payload.playlistId; let pls = state.playlists.map((pl) => { - pl = {...pl}; - if (pl.id === playlistId){ + pl = { ...pl }; + if (pl.id === playlistId) { let maxId = 1; if (Array.isArray(pl.elements)) - // looking for the highest element id to add a higher value to the elements that are being added (this avoid the creation of a new element when the element with id is sent back from the server) - maxId = Math.max(pl.elements.map(el => {return el.id}), 1) + 1; - for (let e in elements){ + // looking for the highest element id to add a higher value to the elements that are being added (this avoid the creation of a new element when the element with id is sent back from the server) + maxId = Math.max(pl.elements.map(el => { return el.id }), 1) + 1; + for (let e in elements) { elements[e].id = maxId++; } pl.elements = [...pl.elements]; @@ -33,34 +33,35 @@ const playlistsSlice = createSlice({ } return pl; }); - return {...state, playlists: pls, mandatoryRefresh: true, playlistAddedNewElement: true }; + return { ...state, playlists: pls, mandatoryRefresh: true, playlistAddedNewElement: true }; }, deletePlaylist: (state, action) => { - return { ...state, playlists: state.playlists.filter((item) => { - return item.id !== action.payload; - })} + return { + ...state, playlists: state.playlists.filter((item) => { + return item.id !== action.payload; + }) + } }, resetPlaylistDeletedFlag: (state, action) => { - return {...state, playlistDeleted: false }; + return { ...state, playlistDeleted: false }; }, resetMandatoryRefresh: (state, action) => { - return {...state, mandatoryRefresh: false}; + return { ...state, mandatoryRefresh: false }; }, setPlaylists: (state, action) => { let playlistDeleted = true; // to check if the playlist has been deleted from someone else - let pls = action.payload.map((pl)=>{ - if (pl.id === state.playlistId){ + let pls = action.payload.map((pl) => { + if (pl.id === state.playlistId) { playlistDeleted = false; } - pl.elements = JSON.parse(pl.elements); return pl; }); - return { - ...state, - playlists: pls, - playlistDeleted: playlistDeleted, + return { + ...state, + playlists: pls, + playlistDeleted: playlistDeleted, mandatoryRefresh: true - }; + }; }, setSinglePlaylistId: (state, action) => { return { ...state, playlistId: action.payload, mandatoryRefresh: true, showNewPlaylist: false }; @@ -70,11 +71,11 @@ const playlistsSlice = createSlice({ let version = 0; let isNew = true; let res = state.playlists.map((pl) => { - if (pl.id === playlist.id){ + if (pl.id === playlist.id) { version = pl.version; isNew = false; return playlist; - }else{ + } else { return pl; } }); @@ -87,9 +88,9 @@ const playlistsSlice = createSlice({ res.push(playlist) // check if it is necessary to refresh the playlist view let mustRefresh = (playlist.id === state.playlistId) && ((playlist.version > version) || state.playlistAddedNewElement); - return { ...state, playlists: res, playlistDeleted: false, mandatoryRefresh: mustRefresh, playlistAddedNewElement: false}; + return { ...state, playlists: res, playlistDeleted: false, mandatoryRefresh: mustRefresh, playlistAddedNewElement: false }; }, - setShowNewPlaylist(state, action){ + setShowNewPlaylist(state, action) { return { ...state, showNewPlaylist: action.payload } } } diff --git a/frontend/src/structure/tabs/queue/Queue.slice.js b/frontend/src/structure/tabs/queue/Queue.slice.js index 1d1699fb..826be10a 100644 --- a/frontend/src/structure/tabs/queue/Queue.slice.js +++ b/frontend/src/structure/tabs/queue/Queue.slice.js @@ -8,49 +8,49 @@ const queueSlice = createSlice({ repeat: false, shuffle: false, interval: 0, - status: {eta: -1} + status: { eta: -1 } }, reducers: { - setInterval(state, action){ + setInterval(state, action) { return { - ...state, + ...state, interval: action.payload } }, - setQueueElements(state, action){ + setQueueElements(state, action) { return { ...state, elements: action.payload } }, - setQueueStatus(state, action){ + setQueueStatus(state, action) { let res = action.payload; res.current_element = res.current_element === "None" ? undefined : JSON.parse(res.current_element); return { - elements: res.elements, + elements: res.elements, currentElement: res.current_element, - interval: res.interval, - status: res.status, - repeat: res.repeat, - shuffle: res.shuffle + interval: res.interval, + status: res.status, + repeat: res.repeat, + shuffle: res.shuffle } }, - toggleQueueShuffle(state, action){ + toggleQueueShuffle(state, action) { return { ...state, shuffle: !state.shuffle } }, - toggleQueueRepeat(state, action){ + toggleQueueRepeat(state, action) { return { - ...state, + ...state, repeat: !state.repeat } } } }); -export const{ +export const { setInterval, setQueueElements, setQueueStatus, diff --git a/frontend/src/structure/tabs/queue/selector.js b/frontend/src/structure/tabs/queue/selector.js index 02d637c2..41af5f21 100644 --- a/frontend/src/structure/tabs/queue/selector.js +++ b/frontend/src/structure/tabs/queue/selector.js @@ -1,39 +1,39 @@ //returns true if the queue is empty -const getQueueEmpty = state => {return state.queue.elements.length === 0}; +const getQueueEmpty = state => { return state.queue.elements.length === 0 }; // returns the list of elements in the queue -const getQueueElements = state => {return state.queue.elements}; +const getQueueElements = state => { return state.queue.elements }; // returns the currently used element -const getQueueCurrent = state => {return state.queue.currentElement}; +const getQueueCurrent = state => { return state.queue.currentElement }; // returns the progress {eta, units} -const getQueueProgress = state => {return state.queue.status.progress}; +const getQueueProgress = state => { return state.queue.status.progress }; // returns true if the feeder is paused -const getIsQueuePaused = state => {return state.queue.status.is_paused}; +const getIsQueuePaused = state => { return state.queue.status.paused }; // returns true if the repeat mode is currently selected -const getQueueRepeat = state => {return state.queue.repeat} +const getQueueRepeat = state => { return state.queue.repeat } // returns true if the shuffle mode is currently selected -const getQueueShuffle = state => {return state.queue.shuffle} +const getQueueShuffle = state => { return state.queue.shuffle } // returns true if the server is running, false, if is on hold -const getQueueIsRunning = state => {return state.queue.status.is_running} +const getQueueIsRunning = state => { return state.queue.status.running } // returns the current interval value for the queue -const getIntervalValue = state => {return state.queue.interval} +const getIntervalValue = state => { return state.queue.interval } export { - getQueueEmpty, - getQueueElements, - getQueueCurrent, - getQueueProgress, - getIsQueuePaused, - getQueueRepeat, - getQueueShuffle, - getQueueIsRunning, + getQueueEmpty, + getQueueElements, + getQueueCurrent, + getQueueProgress, + getIsQueuePaused, + getQueueRepeat, + getQueueShuffle, + getQueueIsRunning, getIntervalValue }; \ No newline at end of file diff --git a/server/__init__.py b/server/__init__.py index 476d95b6..950b958a 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -156,4 +156,4 @@ def run_post(): app.observer = GcodeObserverManager("./server/autodetect", logger=app.logger) if __name__ == "__main__": - socketio.run(app) + socketio.run(app, threaded=True) diff --git a/server/database/models.py b/server/database/models.py index a944fc75..7f7f35f9 100644 --- a/server/database/models.py +++ b/server/database/models.py @@ -142,7 +142,7 @@ def to_json(self): return json.dumps( { "name": self.name, - "elements": self.get_elements_json(), + "elements": [e.get_dict() for e in self.get_elements()], "id": self.id, "version": self.version, } diff --git a/server/hardware/device/comunication/emulator.py b/server/hardware/device/comunication/emulator.py index 426a27b9..f7dbaa73 100644 --- a/server/hardware/device/comunication/emulator.py +++ b/server/hardware/device/comunication/emulator.py @@ -9,6 +9,8 @@ class Emulator: + """Emulates a device""" + def __init__(self): self.feedrate = 5000.0 self.ack_buffer = deque() # used for the standard "ok" acks timing diff --git a/server/hardware/device/estimation/generic_estimator.py b/server/hardware/device/estimation/generic_estimator.py index ea65f2e9..3c157dc2 100644 --- a/server/hardware/device/estimation/generic_estimator.py +++ b/server/hardware/device/estimation/generic_estimator.py @@ -53,6 +53,10 @@ def position(self, pos): raise ValueError("The position given must have both the x and y coordinates") self._position = DotMap(pos) + @property + def feedrate(self): + return self._feedrate + def get_last_commanded_position(self): """ Returns: @@ -79,7 +83,7 @@ def parse_command(self, command): if any(code in command for code in KNOWN_COMMANDS): if "F" in command: - self.feedrate = float(self._feed_regex.findall(command)[0][0]) + self._feedrate = float(self._feed_regex.findall(command)[0][0]) if "X" in command: self._position.x = float(self._x_regex.findall(command)[0][0]) if "Y" in command: diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index 7b7d1c8f..be65f514 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -117,6 +117,9 @@ def init_device(self, settings): Args: settings: the settings dict """ + with self._mutex: + if self._status.running: + self.stop() # close connection with previous settings if available if not self._device is None: if self._device.is_connected(): @@ -162,11 +165,10 @@ def status(self): """ with self._mutex: self._status.progress = ( - self._current_element.get_progress(1000) + self._current_element.get_progress(self._device.estimator.feedrate) if not self._current_element is None else UNKNOWN_PROGRESS ) - # FIXME use feedrate in get_progress argument!! return self._status @property @@ -213,10 +215,12 @@ def stop(self): self._status.running = False if not self._stopped: self.logger.info("Stopping drawing") - while True: + while True: + with self._mutex: if self._stopped: break + with self._mutex: # waiting comand buffer to be cleared before calling the "drawing ended" event while True: if len(self._device.buffer) == 0: @@ -329,7 +333,6 @@ def __thf(self): # execute the command (iterate over the lines/commands or just execute what is necessary) for k, line in enumerate(self._current_element.execute(self.logger)): - self.logger.info("Test2") # if the feeder is being stopped the running flag will be False -> should exit the loop immediately with self._mutex: if not self._status.running: diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index 9947e579..186fd3bf 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -432,7 +432,7 @@ def send_queue_status(self): res = { "current_element": str(self._element), "elements": elements, - "status": self.app.feeder.status, + "status": self.app.feeder.status.toDict(), "repeat": self.repeat, "shuffle": self.shuffle, "interval": self.interval, @@ -445,7 +445,8 @@ def _start_element(self, element): """ with self._mutex: # check if a new element must be generated from the given element (like for a shuffle element) - element = element.before_start(self.app) + if not element is None: + element = element.before_start(self.app) if not element is None: self.app.logger.info("Sending gcode start command") self.app.feeder.start_element(element) diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index 61c4db2b..1be7d6f4 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -107,7 +107,7 @@ def settings_save(data, is_connect): if is_connect: app.logger.info("Connecting device") - app.feeder.connect() + app.feeder.init_device(settings) if app.feeder.is_connected(): app.semits.show_toast_on_UI("Connection to device successful") else: From 46dc9a7826dc90a5a6eb7cd8761590208ba3deeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Feb 2022 12:21:09 +0100 Subject: [PATCH 15/52] Bump follow-redirects from 1.14.6 to 1.14.8 in /frontend (#70) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Luca Tessaro --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 78d790db..0d1ef6e7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5170,9 +5170,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0: - version "1.14.7" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== for-in@^1.0.2: version "1.0.2" From 2589cc4a8e80fb1b4d02dc7134a7518e2fd9f54e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Feb 2022 12:22:05 +0100 Subject: [PATCH 16/52] Bump url-parse from 1.5.3 to 1.5.7 in /frontend (#71) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0d1ef6e7..71929905 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -11216,9 +11216,9 @@ url-loader@4.1.1: schema-utils "^3.0.0" url-parse@^1.4.3, url-parse@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" - integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + version "1.5.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a" + integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" From 39afdeb1b893665403372ede59e0f974c151ed26 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 20 Feb 2022 14:57:55 +0100 Subject: [PATCH 17/52] Fixing tests. --- requirements.txt | 1 - server/tests/test_pages.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e19a5440..7280afae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,6 @@ pylint-plugin-utils==0.7 pyparsing==3.0.6 pyserial==3.5 pytest==6.2.5 -pytest-timeout==2.1.0 python-dateutil==2.8.2 python-dotenv==0.19.2 python-editor==1.0.4 diff --git a/server/tests/test_pages.py b/server/tests/test_pages.py index 608a53a3..e7965ae3 100644 --- a/server/tests/test_pages.py +++ b/server/tests/test_pages.py @@ -1,5 +1,6 @@ - +""" def test_index(client): rv = client.get('/') assert rv.default_status == 200 +""" From fcbb09da42cc1a45e5fcfc126a61c2cad0613d99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Mar 2022 21:14:53 +0100 Subject: [PATCH 18/52] Bump url-parse from 1.5.3 to 1.5.10 in /frontend (#72) Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.10. - [Release notes](https://github.com/unshiftio/url-parse/releases) - [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.10) --- updated-dependencies: - dependency-name: url-parse dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Luca Tessaro --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 71929905..40b26f54 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -11216,9 +11216,9 @@ url-loader@4.1.1: schema-utils "^3.0.0" url-parse@^1.4.3, url-parse@^1.5.3: - version "1.5.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a" - integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA== + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" From 1307d0cf5b22bc7edaab485870bd74c6bc42678e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Mar 2022 21:15:21 +0100 Subject: [PATCH 19/52] Bump pillow from 9.0.0 to 9.0.1 (#73) Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.0 to 9.0.1. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.0.0...9.0.1) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7280afae..448285b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ mypy-extensions==0.4.3 netifaces==0.11.0 packaging==21.3 pathspec==0.9.0 -Pillow==9.0.0 +Pillow==9.0.1 pip==21.3.1 platformdirs==2.4.1 pluggy==1.0.0 From 23ba9b0c3dc788146930a41f8680dade43d0490d Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 24 Mar 2022 22:00:30 +0100 Subject: [PATCH 20/52] Fixed server getting stuck at sending pages after startup --- server/__init__.py | 1 - .../device/comunication/device_serial.py | 12 +++++++-- .../hardware/device/comunication/emulator.py | 27 ++++++++++++++++--- server/hardware/device/feeder.py | 7 +++-- .../device/firmwares/generic_firmware.py | 19 ++++++++----- server/hardware/leds/leds_controller.py | 2 +- .../light_sensors/generic_light_sensor.py | 23 ++++++++-------- server/hardware/leds/light_sensors/tsl2591.py | 1 + .../sockets_interface/socketio_callbacks.py | 2 +- server/tests/test_pages.py | 4 +-- 10 files changed, 64 insertions(+), 34 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index 950b958a..bc6e3ce1 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -143,7 +143,6 @@ def home(): # Starting the feeder after the server is ready to avoid problems with the web page not showing up def run_post(): sleep(2) - # app.feeder.connect() app.lmanager.start() diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 63fdcd0e..a855ba62 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -1,4 +1,5 @@ from threading import Thread, RLock +from time import sleep import serial.tools.list_ports import serial import sys @@ -27,6 +28,7 @@ def __init__(self, serial_name=None, baudrate=115200, logger_name=None): self.serialname = serial_name self.baudrate = baudrate self.is_virtual = False + self.serial = None self._buffer = bytearray() self.echo = "" self._emulator = Emulator() @@ -73,6 +75,7 @@ def set_on_readline_callback(self, callback): """ self._on_readline = callback + @property def is_running(self): """ Check if the reading thread is running @@ -110,6 +113,7 @@ def send(self, line): self.close() self.logger.error("Error while sending a command") + @property def is_connected(self): """ Returns: @@ -149,12 +153,16 @@ def _thf(self): Thread function for the readline """ self._running = True - next_line = "" - while self.is_running(): + next_line = None + + while self.is_running: + # do not understand why but with the emulator need this to make everything work correctly + sleep(0.001) with self._mutex: next_line = self._readline() # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method self._on_readline(next_line) + next_line = None @classmethod def get_serial_port_list(cls): diff --git a/server/hardware/device/comunication/emulator.py b/server/hardware/device/comunication/emulator.py index f7dbaa73..d800ef81 100644 --- a/server/hardware/device/comunication/emulator.py +++ b/server/hardware/device/comunication/emulator.py @@ -1,4 +1,6 @@ -import time, re, math +import time +import re +import math from collections import deque from server.utils.settings_utils import load_settings @@ -24,15 +26,28 @@ def __init__(self): self.settings = load_settings() def get_x(self, line): + """ + Return the x value of the command if available in the given line + """ return float(self.xr.findall(line)[0][0]) def get_y(self, line): + """ + Return the y value of the command if available in the given line + """ return float(self.yr.findall(line)[0][0]) def _buffer_empty(self): + """Return True if the buffer is empty""" return len(self.ack_buffer) < 1 def send(self, command): + """ + Used to send a command to the emulator + + Args: + - command: the command sent to the emulator + """ if self._buffer_empty(): self.last_time = time.time() @@ -80,6 +95,9 @@ def send(self, command): self.message_buffer.append(ACK) def readline(self): + """ + Readline method for the emulated device. Used by the serial controller + """ # special commands response if len(self.message_buffer) >= 1: return self.message_buffer.popleft() @@ -87,9 +105,10 @@ def readline(self): # standard lines acks (G0, G1) if self._buffer_empty(): return None - oldest = self.ack_buffer.popleft() + oldest = 1000000000 + if len(self.ack_buffer): + oldest = self.ack_buffer.popleft() if oldest > time.time(): self.ack_buffer.appendleft(oldest) return None - else: - return ACK + return ACK diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index be65f514..bccfba78 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -122,7 +122,7 @@ def init_device(self, settings): self.stop() # close connection with previous settings if available if not self._device is None: - if self._device.is_connected(): + if self._device.is_connected: self._device.close() self.settings = settings @@ -145,6 +145,7 @@ def init_device(self, settings): # if the device is not available will create a fake/virtual device self._device.connect() + @property def is_connected(self): """ Returns: @@ -152,7 +153,7 @@ def is_connected(self): False if is using a virtual device """ with self._mutex: - return self._device.is_connected() + return self._device.is_connected @property def status(self): @@ -303,11 +304,9 @@ def update_current_time_element(self, new_interval): # event handler methods def on_line_sent(self, line): - self.logger.log(settings_utils.LINE_SENT, line) self.event_handler.on_new_line(line) def on_line_received(self, line): - self.logger.log(settings_utils.LINE_RECEIVED, line) self.event_handler.on_message_received(line) def on_device_ready(self): diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index 22df62e8..dddee588 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -2,6 +2,7 @@ import re from threading import RLock, Lock +import time from py_expression_eval import Parser from server.hardware.device.comunication.device_serial import DeviceSerial @@ -113,6 +114,7 @@ def buffer(self): with self._mutex: return self._buffer + @property def is_ready(self): """ Returns: @@ -122,12 +124,13 @@ def is_ready(self): with self._mutex: return self._is_ready + @property def is_connected(self): """ Returns: True if the device is connected """ - return self._serial_device.is_connected() + return self._serial_device.is_connected def get_current_position(self): """ @@ -166,7 +169,8 @@ def connect(self): self._serial_device.set_on_readline_callback(self._on_readline) self._serial_device.open() # wait device ready - if not self._serial_device.is_connected(): + if not self._serial_device.is_connected: + time.sleep(1) self._on_device_ready() def close(self): @@ -311,11 +315,11 @@ def _on_readline(self, line): Returns: True if the readline has done correctly """ - with self._mutex: - if line is None: - return False - if self.ack in line: - self._buffer.ack_received() + # with self._mutex: + if line is None: + return False + if self.ack in line: + self._buffer.ack_received() return True def _on_device_ready(self): @@ -336,6 +340,7 @@ def _log_received_line(self, line, hide_line=False): line: the line received from the device hide_line: if True will not send the line to the frontend """ + line = line.rstrip("\r").rstrip("\n") self._logger.log(settings_utils.LINE_RECEIVED, line) if not hide_line: self.event_handler.on_line_received(line) diff --git a/server/hardware/leds/leds_controller.py b/server/hardware/leds/leds_controller.py index b532bd6a..4ad33abe 100644 --- a/server/hardware/leds/leds_controller.py +++ b/server/hardware/leds/leds_controller.py @@ -31,7 +31,7 @@ def is_available(self): def has_light_sensor(self): if not self.sensor is None: - return self.sensor.is_connected() + return self.sensor.is_connected return False def start(self): diff --git a/server/hardware/leds/light_sensors/generic_light_sensor.py b/server/hardware/leds/light_sensors/generic_light_sensor.py index 19dfa2d5..bfe2d4a1 100644 --- a/server/hardware/leds/light_sensors/generic_light_sensor.py +++ b/server/hardware/leds/light_sensors/generic_light_sensor.py @@ -2,27 +2,27 @@ from threading import Thread from time import sleep -BRIGHTNESS_MOV_AVE_SAMPLES = 20 # number of samples used in the moving average for the brightness (response time [s] ~ samples_number*sample_interval) -BRIGHTNESS_SAMPLE_INTERVAL = 0.5 # period in s for the brightness sampling with the sensor +BRIGHTNESS_MOV_AVE_SAMPLES = 20 # number of samples used in the moving average for the brightness (response time [s] ~ samples_number*sample_interval) +BRIGHTNESS_SAMPLE_INTERVAL = 0.5 # period in s for the brightness sampling with the sensor -class GenericLightSensor(ABC): +class GenericLightSensor(ABC): def __init__(self, app): self.app = app self._is_running = False self._check_interval = BRIGHTNESS_SAMPLE_INTERVAL self._history = [] - + def start(self): """Starts the light sensor - + When the light sensor is started, will control the brightness of the LEDs automatically. Will change it according to the last given color (can only dim)""" self._is_running = True - self._th = Thread(target = self._thf) + self._th = Thread(target=self._thf) self._th.name = "light_sensor" self._th.start() - + def stop(self): """Stops the light sensor from controlling the LED strip""" self._is_running = False @@ -36,12 +36,12 @@ def _thf(self): if len(self._history) == BRIGHTNESS_MOV_AVE_SAMPLES: self._history.pop(0) self._history.append(brightness) - brightness = sum(self._history)/float(len(self._history)) + brightness = sum(self._history) / float(len(self._history)) - self.app.logger.info("Averaged brightness: {}".format(brightness)) # FIXME remove this + self.app.logger.info("Averaged brightness: {}".format(brightness)) # FIXME remove this self.app.lmanager.set_brightness(brightness) self.app.lmanager.set_brightness(1) - + def deinit(self): """Deinitializes the sensor hw""" @@ -51,6 +51,7 @@ def deinit(self): def get_brightness(self): """Returns the actual level of brightness to use""" + @property @abstractmethod def is_connected(self): - """Returns true if the sensor is connected correctly""" \ No newline at end of file + """Returns true if the sensor is connected correctly""" diff --git a/server/hardware/leds/light_sensors/tsl2591.py b/server/hardware/leds/light_sensors/tsl2591.py index 47633d68..25229c93 100644 --- a/server/hardware/leds/light_sensors/tsl2591.py +++ b/server/hardware/leds/light_sensors/tsl2591.py @@ -28,5 +28,6 @@ def get_brightness(self): self.app.logger.info("Sensor current brightness: {}".format(tmp)) # FIXME remove this return tmp + @property def is_connected(self): return not self._sensor is None diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index 1be7d6f4..337264fa 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -108,7 +108,7 @@ def settings_save(data, is_connect): app.logger.info("Connecting device") app.feeder.init_device(settings) - if app.feeder.is_connected(): + 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 virtual serial port.") diff --git a/server/tests/test_pages.py b/server/tests/test_pages.py index e7965ae3..3a65d2e6 100644 --- a/server/tests/test_pages.py +++ b/server/tests/test_pages.py @@ -1,6 +1,4 @@ -""" def test_index(client): - rv = client.get('/') + rv = client.get("/") assert rv.default_status == 200 -""" From f6b1ae9f0183208ae87b56867b2d23f2003e3302 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 24 Mar 2022 22:10:52 +0100 Subject: [PATCH 21/52] Cleanup --- .../device/firmwares/generic_firmware.py | 3 +- test.py | 48 ------------------- 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 test.py diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index dddee588..20706cd7 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -323,9 +323,8 @@ def _on_readline(self, line): return True def _on_device_ready(self): - print("test") """ - Called when the connected device is ready to receive commands + Called when the connected device is ready to receive commands """ with self._mutex: self.event_handler.on_device_ready() diff --git a/test.py b/test.py deleted file mode 100644 index 47ace833..00000000 --- a/test.py +++ /dev/null @@ -1,48 +0,0 @@ -# FIXME remove this - -import time -from server.hardware.device.firmwares.grbl import Grbl -from server.hardware.device.firmwares.marlin import Marlin -from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler -import logging - -if __name__ == "__main__": - - def test_device(device): - device.connect() - device.send_gcode_command("G28") - device.send_gcode_command("G0 X0 Y0 F300") - for x in range(15): - device.send_gcode_command(f"G0 X{x*2} Y0") - - class EventHandler(FirwmareEventHandler): - def on_line_received(self, line): - print(f"Line received: {line}") - - def on_line_sent(self, line): - print(f"Line sent: {line}") - - def on_device_ready(self): - print("Device ready") - - settings = {"port": {"value": "COM3"}, "baud": {"value": 115200}} - print("Testing Marlin") - - logger_name = logging.getLogger().name - device = Marlin( - serial_settings=settings, - logger=logger_name, - event_handler=EventHandler(), - ) - - test_device(device) - - print("Marlin done") - - print("Testing grbl") - - device = Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler()) - - # test_device(device) - - print("Grbl done") From 26bf100215aa2221b1b54aebddeb78e708bb8f47 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 11:26:06 +0200 Subject: [PATCH 22/52] Updating pytest workflow to check where and why it stops --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1d9024d9..4bb4cac9 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -39,4 +39,4 @@ jobs: flask db upgrade - name: Test with pytest run: | - python -m pytest server/tests + python -m pytest -s -v server/tests From d310330f6c4ee6cbe064384c8d3b49ef3e424072 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 11:36:24 +0200 Subject: [PATCH 23/52] Fixing possible infinite recursion in the queue manager --- server/hardware/queue_manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index 186fd3bf..de46d2b1 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -305,6 +305,9 @@ def queue_element(self, element, show_toast=True): self.send_queue_status() def set_element_ended(self): + """ + Set the current element as ended and start the next element if the queue is not empty + """ with self._mutex: # if the ended element was forced to stop should not set the "last_time" otherwise when a new element is started there will be a delay element first if self._is_force_stop: @@ -418,7 +421,8 @@ def start_next(self, force_stop=False): str(e) ) ) - self.start_next() + if self.get_queue_len() != 0: + self.start_next() def send_queue_status(self): """ From cc8ec0c3b3404ba623d462f7ed605dc9c734cd1d Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 11:49:49 +0200 Subject: [PATCH 24/52] Testing if the pytest problem has to do with the logger --- server/hardware/queue_manager.py | 4 ++-- server/utils/logging_utils.py | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index de46d2b1..ad5d0f5b 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -415,12 +415,12 @@ def start_next(self, force_stop=False): self.app.logger.info("Starting next element: {}".format(next_element)) except Exception as e: - self.app.logger.exception(e) + """self.app.logger.exception(e) self.app.logger.error( "An error occured while starting a new drawing from the queue:\n{}".format( str(e) ) - ) + )""" if self.get_queue_len() != 0: self.start_next() diff --git a/server/utils/logging_utils.py b/server/utils/logging_utils.py index b7256f6a..1899e1db 100644 --- a/server/utils/logging_utils.py +++ b/server/utils/logging_utils.py @@ -1,22 +1,24 @@ from logging import Formatter import logging -from logging.handlers import RotatingFileHandler, QueueHandler, QueueListener -from queue import Queue -from multiprocessing import RLock +from logging.handlers import RotatingFileHandler import shutil -# creating a custom multiprocessing rotating file handler -# https://stackoverflow.com/questions/32099378/python-multiprocessing-logging-queuehandler-with-rotatingfilehandler-file-bein class MultiprocessRotatingFileHandler(RotatingFileHandler): + """ + Multiprocessing rotating gile handler + https://stackoverflow.com/questions/32099378/python-multiprocessing-logging-queuehandler-with-rotatingfilehandler-file-bein + """ + def __init__(self, *kargs, **kwargs): super(MultiprocessRotatingFileHandler, self).__init__(*kargs, **kwargs) - # not sure why but the .log file was seen already open when it was necessary to rotate to a new file. - # instead of renaming the file now I'm copying the entire file to the new log.1 file and the clear the original .log file - # this is for sure not the best solution but it looks like it is working now def rotate(self, source, dest): + """Rotate to a new file""" + # not sure why but the .log file was seen already open when it was necessary to rotate to a new file. + # instead of renaming the file now I'm copying the entire file to the new log.1 file and the clear the original .log file + # this is for sure not the best solution but it looks like it is working now shutil.copyfile(source, dest) - f = open(source, "r+") + f = open(source, "r+", encoding="utf-8") f.truncate(0) From 19ee8a49431feb46948b59c6f23f8859b6964b0a Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 11:56:02 +0200 Subject: [PATCH 25/52] Testing without last button test --- server/hardware/queue_manager.py | 4 ++-- server/tests/test_buttons.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index ad5d0f5b..de46d2b1 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -415,12 +415,12 @@ def start_next(self, force_stop=False): self.app.logger.info("Starting next element: {}".format(next_element)) except Exception as e: - """self.app.logger.exception(e) + self.app.logger.exception(e) self.app.logger.error( "An error occured while starting a new drawing from the queue:\n{}".format( str(e) ) - )""" + ) if self.get_queue_len() != 0: self.start_next() diff --git a/server/tests/test_buttons.py b/server/tests/test_buttons.py index 13f09d3a..1a374524 100644 --- a/server/tests/test_buttons.py +++ b/server/tests/test_buttons.py @@ -26,9 +26,9 @@ def test_buttons_gpio_available(): # checking if the button actions are created correctly and also if the execute method has been overwritten -def test_buttons_action_has_execute(): - for cl in inspect.getmembers(button_actions, inspect.isclass): - if not cl[1] is GenericButtonAction: - print(cl[0]) - a = cl[1](app) - a.execute() +# def test_buttons_action_has_execute(): +# for cl in inspect.getmembers(button_actions, inspect.isclass): +# if not cl[1] is GenericButtonAction: +# print(cl[0]) +# a = cl[1](app) +# a.execute() From f5909e23fa6fe0bc4ae7534a7d4df2e069605238 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 12:01:13 +0200 Subject: [PATCH 26/52] Removing single command from buttons to check if it giving the error --- .../sockets_interface/socketio_callbacks.py | 2 +- server/tests/test_buttons.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index 337264fa..49786f27 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -230,7 +230,7 @@ def queue_set_order(elements): @socketio.on("queue_next_drawing") def queue_next_drawing(): app.semits.show_toast_on_UI("Stopping drawing...") - app.qmanager.start_next(force_stop=True) + # app.qmanager.start_next(force_stop=True) if ( not app.qmanager.is_drawing() ): # if the drawing was the last in the queue must send the updated status diff --git a/server/tests/test_buttons.py b/server/tests/test_buttons.py index 1a374524..ea0c008d 100644 --- a/server/tests/test_buttons.py +++ b/server/tests/test_buttons.py @@ -6,6 +6,7 @@ def test_buttons_get_options(): + """Check that all the actions have the correct information to be sent to the frontend""" options = app.bmanager.get_buttons_options() fields = ["description", "label", "name", "usage"] for o in options: @@ -20,15 +21,14 @@ def test_buttons_get_options(): def test_buttons_gpio_available(): - assert ( - not app.bmanager.gpio_is_available() - ) # the test must pass on a linux device not using real hw + """This test must pass on a linux device that is not using real hw""" + assert not app.bmanager.gpio_is_available() -# checking if the button actions are created correctly and also if the execute method has been overwritten -# def test_buttons_action_has_execute(): -# for cl in inspect.getmembers(button_actions, inspect.isclass): -# if not cl[1] is GenericButtonAction: -# print(cl[0]) -# a = cl[1](app) -# a.execute() +def test_buttons_action_has_execute(): + """checking if the button actions are created correctly and also if the execute method has been overwritten""" + for cl in inspect.getmembers(button_actions, inspect.isclass): + if not cl[1] is GenericButtonAction: + print(cl[0]) + a = cl[1](app) + a.execute() From e8f8eed7c6f61cad02e9b49e54b18770fc936a7e Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 12:07:29 +0200 Subject: [PATCH 27/52] Removing action from start next --- server/hardware/buttons/actions.py | 2 +- server/hardware/queue_manager.py | 9 ++++++--- server/sockets_interface/socketio_callbacks.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/server/hardware/buttons/actions.py b/server/hardware/buttons/actions.py index 53bdbf10..d160dbc8 100644 --- a/server/hardware/buttons/actions.py +++ b/server/hardware/buttons/actions.py @@ -39,7 +39,7 @@ class StartNext(GenericButtonAction): def execute(self): from server.sockets_interface.socketio_callbacks import queue_next_drawing - queue_next_drawing() + # queue_next_drawing() class BrightnessUp(GenericButtonAction): diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index de46d2b1..e1b5e3b6 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -414,11 +414,14 @@ def start_next(self, force_stop=False): self._start_element(next_element) self.app.logger.info("Starting next element: {}".format(next_element)) - except Exception as e: - self.app.logger.exception(e) + except RecursionError as exception: + self.app.logger.exception(exception) + return + except Exception as exception: + self.app.logger.exception(exception) self.app.logger.error( "An error occured while starting a new drawing from the queue:\n{}".format( - str(e) + str(exception) ) ) if self.get_queue_len() != 0: diff --git a/server/sockets_interface/socketio_callbacks.py b/server/sockets_interface/socketio_callbacks.py index 49786f27..337264fa 100644 --- a/server/sockets_interface/socketio_callbacks.py +++ b/server/sockets_interface/socketio_callbacks.py @@ -230,7 +230,7 @@ def queue_set_order(elements): @socketio.on("queue_next_drawing") def queue_next_drawing(): app.semits.show_toast_on_UI("Stopping drawing...") - # app.qmanager.start_next(force_stop=True) + app.qmanager.start_next(force_stop=True) if ( not app.qmanager.is_drawing() ): # if the drawing was the last in the queue must send the updated status From 6ace0a9ae7bb1e2426c661699ea6322433074f65 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 12:38:32 +0200 Subject: [PATCH 28/52] Restored actions --- server/hardware/buttons/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/hardware/buttons/actions.py b/server/hardware/buttons/actions.py index d160dbc8..53bdbf10 100644 --- a/server/hardware/buttons/actions.py +++ b/server/hardware/buttons/actions.py @@ -39,7 +39,7 @@ class StartNext(GenericButtonAction): def execute(self): from server.sockets_interface.socketio_callbacks import queue_next_drawing - # queue_next_drawing() + queue_next_drawing() class BrightnessUp(GenericButtonAction): From 7bf2d0d3bb38a3aef5d4fcbc3c6ed6444b6d8e50 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 14:38:33 +0200 Subject: [PATCH 29/52] Fixes for the docker build --- docker/start_server.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/start_server.sh b/docker/start_server.sh index 71cabee2..a898ec24 100644 --- a/docker/start_server.sh +++ b/docker/start_server.sh @@ -1,11 +1,11 @@ #!/bin/bash # check if .env files needs to be replaced -python check_prestart.py +python check_prestart.py # Need to update the flask db at runtime -flask db upgrade +flask db upgrade # TODO setup a production server # starts the server -flask run --host=0.0.0.0 \ No newline at end of file +flask run --host=0.0.0.0 \ No newline at end of file From 3c0791548807f3ba3ef05d5bba92bfb2328de572 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 16:36:45 +0200 Subject: [PATCH 30/52] Marlin firmware minor fixes... Still some problems with the stop function for the drawing --- frontend/src/structure/TopBar.js | 108 +++++++++--------- .../tabs/settings/SoftwareVersion.js | 18 +-- .../device/comunication/device_serial.py | 2 +- .../hardware/device/comunication/emulator.py | 1 + server/hardware/device/feeder.py | 17 ++- server/hardware/device/firmwares/marlin.py | 9 +- todos.md | 2 - 7 files changed, 79 insertions(+), 78 deletions(-) diff --git a/frontend/src/structure/TopBar.js b/frontend/src/structure/TopBar.js index eb5ea4fb..6f10395d 100644 --- a/frontend/src/structure/TopBar.js +++ b/frontend/src/structure/TopBar.js @@ -12,7 +12,7 @@ import { showLEDs, systemIsLinux, updateDockerComposeLatest } from './tabs/setti import { settingsRebootSystem, settingsShutdownSystem } from '../sockets/sEmits'; const mapStateToProps = (state) => { - return { + return { showBack: showBack(state), isLinux: systemIsLinux(state), showLEDs: showLEDs(state), @@ -22,34 +22,34 @@ const mapStateToProps = (state) => { } const mapDispatchToProps = (dispatch) => { - return { + return { handleTab: (name) => dispatch(setTab(name)), handleTabBack: () => dispatch(tabBack()) } } -class TopBar extends Component{ +class TopBar extends Component { - renderBack(){ + renderBack() { if (this.props.showBack) - return {this.props.handleTabBack()}}>Back + return { this.props.handleTabBack() }}>Back else return ""; } - renderSettingsButton(){ + renderSettingsButton() { let notificationCounter = 0; let renderedCounter = ""; if (!this.props.dockerComposeUpdateAvailable) notificationCounter++; - if (notificationCounter>0){ - renderedCounter = {notificationCounter} + if (notificationCounter > 0) { + renderedCounter = {notificationCounter} } if (this.props.isLinux) return - {this.props.handleTab("settings")}} + { this.props.handleTab("settings") }} icon={Sliders}> - Settings{renderedCounter} + Settings{renderedCounter} @@ -60,59 +60,59 @@ class TopBar extends Component{ onClick={() => settingsRebootSystem()}>Reboot - else return {this.props.handleTab("settings")}} - icon={Sliders}> - Settings{renderedCounter} - + else return { this.props.handleTab("settings") }} + icon={Sliders}> + Settings{renderedCounter} + } - renderLEDsTab(){ + renderLEDsTab() { if (this.props.showLEDs.value) return {this.props.handleTab("leds")}}> - LEDs + key={5} + onClick={() => { this.props.handleTab("leds") }}> + LEDs else return ""; } - render(){ + render() { return
- + - {this.props.handleTab("home")}}> -

Sandypi

-
- - - - - {this.renderSettingsButton()} - + { this.props.handleTab("home") }}> +

Sandypi

+
+ + + + + {this.renderSettingsButton()} +
} diff --git a/frontend/src/structure/tabs/settings/SoftwareVersion.js b/frontend/src/structure/tabs/settings/SoftwareVersion.js index c9f16ff6..78ba4680 100644 --- a/frontend/src/structure/tabs/settings/SoftwareVersion.js +++ b/frontend/src/structure/tabs/settings/SoftwareVersion.js @@ -23,28 +23,28 @@ const mapDispatchToProps = (dispatch) => { } } -class SoftwareVersion extends Component{ +class SoftwareVersion extends Component { - renderUpdateButton(){ + renderUpdateButton() { if (this.props.updateEnabled) return Disable automatic updates else return Enable automatic updates } - renderDockerComposeUpdate(){ - if (!this.props.dockerComposeLatest){ + renderDockerComposeUpdate() { + if (!this.props.dockerComposeLatest) { return -

-

-

Docker-compose.yml file update available


A new version of the docker-compose file is available but requires to be updated manually. Check the Github homepage to see how to update.
-

+
+
+

Docker-compose.yml file update available


A new version of the docker-compose file is available but requires to be updated manually. Check the Github homepage to see how to update.
+
} return ""; } // todo add docker files version check - render(){ + render() { return

diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index a855ba62..1c146b78 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -157,7 +157,7 @@ def _thf(self): while self.is_running: # do not understand why but with the emulator need this to make everything work correctly - sleep(0.001) + sleep(0.0001) with self._mutex: next_line = self._readline() # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method diff --git a/server/hardware/device/comunication/emulator.py b/server/hardware/device/comunication/emulator.py index d800ef81..14707a89 100644 --- a/server/hardware/device/comunication/emulator.py +++ b/server/hardware/device/comunication/emulator.py @@ -98,6 +98,7 @@ def readline(self): """ Readline method for the emulated device. Used by the serial controller """ + time.sleep(0.001) # special commands response if len(self.message_buffer) >= 1: return self.message_buffer.popleft() diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index bccfba78..dc51eee9 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -221,17 +221,16 @@ def stop(self): if self._stopped: break - with self._mutex: - # waiting comand buffer to be cleared before calling the "drawing ended" event - while True: - if len(self._device.buffer) == 0: - break + # waiting comand buffer to be cleared before calling the "drawing ended" event + while True: + if len(self._device.buffer) == 0: + break - # clean the device status - self._device.reset_status() + # clean the device status + self._device.reset_status() - # call the element ended callback - self.event_handler.on_element_ended(tmp) + # call the element ended callback + self.event_handler.on_element_ended(tmp) def send_gcode_command(self, command): """ diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index eb8888a4..478e120c 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -1,4 +1,5 @@ from copy import deepcopy +from threading import Timer from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler from server.hardware.device.firmwares.generic_firmware import GenericFirmware @@ -114,7 +115,8 @@ def _on_readline(self, line): hide_line = True # the device send a "start" line when ready elif "start" in line: - self._on_device_ready() + # adding delay otherwise there is a collision most of the time (n seconds) + Timer(2, self._on_device_ready).start() # TODO check feedrate response for M220 and set feedrate # elif "_______" in line: # must see the real output from marlin @@ -177,5 +179,6 @@ def _reset_line_number(self, line_number=2): line_number (optional): the line number that should start counting from """ with self._mutex: - self._logger.info("Resetting line number") - self.send_gcode_command("M110 N{}".format(line_number)) + self._logger.info("Clearing buffer and resetting line number") + self._buffer.clear() + self.send_gcode_command(f"M110 N{line_number}") diff --git a/todos.md b/todos.md index 015cbdc6..fb8ed2f6 100644 --- a/todos.md +++ b/todos.md @@ -3,12 +3,10 @@ Here is a brief list of "TODOs". More are available also in the code itself. When using vscode it is possible to use the "todo tree" extension to have a full list. * update the queue tab to show a playlist element that shows at which point of the playlist we are in instead of showing only the element -* highlight which drawing is being used both in the playlists and highlight also the playlist * add the possibility to save the interval between drawings in the playlists * save the interval between drawings in the home/drawings pages such that is loaded instead of being reset every time * add back a button to queue multiple playlists (also with the interval option) * add the possibility to select playlists instead of drawings in the autostart settings -* add the possibility to start/stop/pause with hw buttons (something like: in the settings page possibility to add pins and let select what the pin does from a select list) * add leds control animations * add an leds control element for the playlists * create some automatic tests (both python and js) to validate the software before merging From 80e229ca7fd76b94b99bfef52b813537adc6097f Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 17:02:57 +0200 Subject: [PATCH 31/52] Serial readline refactor to parse a full line --- .../device/comunication/device_serial.py | 19 +++++++++++++------ .../device/firmwares/commands_buffer.py | 1 + server/hardware/device/firmwares/marlin.py | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 1c146b78..3ea06bc0 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -153,16 +153,23 @@ def _thf(self): Thread function for the readline """ self._running = True - next_line = None + next_line = "" + buff = "" while self.is_running: # do not understand why but with the emulator need this to make everything work correctly - sleep(0.0001) with self._mutex: - next_line = self._readline() - # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method - self._on_readline(next_line) - next_line = None + buff = self._readline() + + # reconstruct line from the partial string received + if not buff is None: + next_line += buff + res = next_line.split("\n") if not next_line is None else [] + if len(res) > 1: + # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method + next_line = res[-1] + for l in res[:-1]: + self._on_readline(l) @classmethod def get_serial_port_list(cls): diff --git a/server/hardware/device/firmwares/commands_buffer.py b/server/hardware/device/firmwares/commands_buffer.py index 57ccf849..a1fabb48 100644 --- a/server/hardware/device/firmwares/commands_buffer.py +++ b/server/hardware/device/firmwares/commands_buffer.py @@ -60,6 +60,7 @@ def clear(self): """ with self._mutex: self._buffer.clear() + self._buffer_history.clear() def ack_received(self, safe_line_number=None, append_left_extra=False): """ diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index 478e120c..3b9e05b6 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -53,9 +53,9 @@ def _on_readline(self, line): hide_line = False # Resend - if "Resend: " in line: + if "Error:Line Number" in line: line_found = False - line_number = int(line.replace("Resend: ", "").replace("\r\n", "")) + line_number = int(line.replace("\r\n", "").split(" ")[-1]) + 1 items = deepcopy(self.buffer._buffer_history) first_available_line = None for command_n, command in items.items(): @@ -171,7 +171,7 @@ def _on_device_ready(self): self._reset_line_number() super()._on_device_ready() - def _reset_line_number(self, line_number=2): + def _reset_line_number(self, line_number=3): """ Send a gcode command to reset the line numbering on the device From 382046632fa290252b7658eb444066f2893eea2b Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Thu, 31 Mar 2022 17:38:35 +0200 Subject: [PATCH 32/52] Fixed drawing stop bug for marlin --- .../hardware/device/comunication/device_serial.py | 1 - server/hardware/device/feeder.py | 13 +++++++------ server/hardware/device/firmwares/commands_buffer.py | 3 +++ server/hardware/device/firmwares/marlin.py | 2 +- server/hardware/queue_manager.py | 4 +++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 3ea06bc0..0c2ad8c2 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -1,5 +1,4 @@ from threading import Thread, RLock -from time import sleep import serial.tools.list_ports import serial import sys diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index dc51eee9..52df3d6f 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -222,9 +222,10 @@ def stop(self): break # waiting comand buffer to be cleared before calling the "drawing ended" event - while True: - if len(self._device.buffer) == 0: - break + # while True: + # if len(self._device.buffer) == 0: + # break + # FIXME this is not working correctly, the buffer is not getting cleared correctly for some reason (with marlin at least) # clean the device status self._device.reset_status() @@ -354,6 +355,6 @@ def __thf(self): if isinstance(self._current_element, DrawingElement): self.send_script(self.settings["scripts"]["after"]["value"]) - self._stopped = True - if self._status.running: - self.stop() + self._stopped = True + if self._status.running: + self.stop() diff --git a/server/hardware/device/firmwares/commands_buffer.py b/server/hardware/device/firmwares/commands_buffer.py index a1fabb48..22a18ee4 100644 --- a/server/hardware/device/firmwares/commands_buffer.py +++ b/server/hardware/device/firmwares/commands_buffer.py @@ -5,6 +5,8 @@ class CommandBuffer: + """Buffer to store the commands and keep of the buffer status on the device""" + def __init__(self, max_length=8): """ Args: @@ -61,6 +63,7 @@ def clear(self): with self._mutex: self._buffer.clear() self._buffer_history.clear() + self.check_buffer_mutex_status() def ack_received(self, safe_line_number=None, append_left_extra=False): """ diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index 3b9e05b6..49f5606f 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -180,5 +180,5 @@ def _reset_line_number(self, line_number=3): """ with self._mutex: self._logger.info("Clearing buffer and resetting line number") - self._buffer.clear() + self.buffer.clear() self.send_gcode_command(f"M110 N{line_number}") diff --git a/server/hardware/queue_manager.py b/server/hardware/queue_manager.py index e1b5e3b6..a2a33aae 100644 --- a/server/hardware/queue_manager.py +++ b/server/hardware/queue_manager.py @@ -470,8 +470,10 @@ def _put_random_element_in_queue(self): # TODO move this method into an "startup manager" which will need to initialize queue manager and initial status - # checks if should start drawing after the server is started and ready (can be set in the settings page) def check_autostart(self): + """ + Checks if should start drawing after the server is started and ready (can be set in the settings page) + """ with self._mutex: autostart = settings_utils.get_only_values(settings_utils.load_settings()["autostart"]) From 20583dda72c71d5a8c73f749e2c1f75b8aec998e Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 2 Apr 2022 12:02:34 +0200 Subject: [PATCH 33/52] Fixed some issues with the device_serial class. Now should be much more reliable. Fixed also marlin M110 command handling --- .../device/comunication/device_serial.py | 61 +++++++++++-------- .../device/comunication/readline_buffer.py | 44 +++++++++++++ .../device/firmwares/generic_firmware.py | 6 +- server/hardware/device/firmwares/marlin.py | 7 ++- 4 files changed, 88 insertions(+), 30 deletions(-) create mode 100644 server/hardware/device/comunication/readline_buffer.py diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 0c2ad8c2..a193af73 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -1,11 +1,12 @@ -from threading import Thread, RLock -import serial.tools.list_ports -import serial import sys import logging import glob +from threading import Thread, RLock +import serial +import serial.tools.list_ports from server.hardware.device.comunication.emulator import Emulator +from server.hardware.device.comunication.readline_buffer import ReadlineBuffer class DeviceSerial: @@ -31,6 +32,7 @@ def __init__(self, serial_name=None, baudrate=115200, logger_name=None): self._buffer = bytearray() self.echo = "" self._emulator = Emulator() + self._readline_buffer = ReadlineBuffer() # empty callback function def useless(arg): @@ -60,7 +62,8 @@ def open(self): self.logger.exception(e) self.is_virtual = True self.logger.error( - "Serial not available. Are you sure the device is connected and is not in use by other softwares? (Will use the virtual serial)" + "Serial not available. Are you sure the device is connected and is not in use by other softwares? \ + (Will use the virtual serial)" ) self._th.start() @@ -70,7 +73,8 @@ def set_on_readline_callback(self, callback): Set the a callback for a new line received Args: - callback: the function to call when a new line is received. The function will receive the line as an argument + callback: the function to call when a new line is received. + The function will receive the line as an argument """ self._on_readline = callback @@ -102,12 +106,13 @@ def send(self, line): else: if self.serial.is_open: try: + while self.serial.out_waiting: + pass with self._mutex: - while self.serial.out_waiting: - pass # TODO should add a sort of timeout - self._readline() self.serial.write(str(line).encode()) - # TODO try to send byte by byte instead of a full line? (to reduce the risk of sending commands with missing digits or wrong values that may lead to a wrong position value) + # TODO try to send byte by byte instead of a full line? + # (to reduce the risk of sending commands with missing digits or wrong values + # that may lead to a wrong position value) except: self.close() self.logger.error("Error while sending a command") @@ -139,36 +144,42 @@ def _readline(self): """ Reads a line from the device (if available) and call the callback """ + line = "" if not self.is_virtual: if self.serial.is_open: - while self.serial.in_waiting > 0: + if self.serial.in_waiting > 0: line = self.serial.readline() - return line.decode(encoding="UTF-8") + line = line.decode(encoding="UTF-8") else: - return self._emulator.readline() + line = self._emulator.readline() + + if (line == "") or (line is None): + return + + self._readline_buffer.update_buffer(line) def _thf(self): """ Thread function for the readline """ self._running = True - next_line = "" - buff = "" while self.is_running: # do not understand why but with the emulator need this to make everything work correctly with self._mutex: - buff = self._readline() - - # reconstruct line from the partial string received - if not buff is None: - next_line += buff - res = next_line.split("\n") if not next_line is None else [] - if len(res) > 1: - # cannot use the callback inside the mutex otherwise may run into a deadlock with the mutex if the serial.send is called in the parsing method - next_line = res[-1] - for l in res[:-1]: - self._on_readline(l) + self._readline() + + # check if should use the callback when there is a new full line + full_lines = self._readline_buffer.full_lines + # use the callback for every full line available + for full_line in full_lines: + try: + self._on_readline(full_line) + except Exception as exception: + self.logger.error( + f"Exception while raising the readline callback on line '{full_line}'" + ) + self.logger.exception(exception) @classmethod def get_serial_port_list(cls): diff --git a/server/hardware/device/comunication/readline_buffer.py b/server/hardware/device/comunication/readline_buffer.py new file mode 100644 index 00000000..e572494b --- /dev/null +++ b/server/hardware/device/comunication/readline_buffer.py @@ -0,0 +1,44 @@ +class ReadlineBuffer: + """ + This buffer handles the data received from the serial + + The received data is stored and the returned value is different than None only if + there is a newline character + """ + + def __init__(self): + self._buff = "" + self._full_lines = [] + + def update_buffer(self, new_bytes): + """ + Update the buffer with the last received bytes + + Args: + new_bytes: the freshly received bytes from the serial + """ + if (new_bytes == "") or (new_bytes is None): + return + + self._buff += new_bytes + tmp = self._buff.split("\n") + if len(tmp) > 1: + # setting the buffer to use the last received line bit + self._buff = tmp[-1] + # adding current lines to the full list of lines to check + self._full_lines += tmp[:-1] + + @property + def full_lines(self): + """ + Returns the list of full lines received and then will clear the list + + Returns: + list of full lines that have been received + """ + if len(self._full_lines) == 0: + return [] + + tmp = self._full_lines + self._full_lines = [] + return tmp diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index 20706cd7..bceb11b7 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -186,7 +186,7 @@ def emergency_stop(self): This method must be implemented in the child class """ - pass + ... def reset_status(self): """ @@ -215,10 +215,10 @@ def _parse_macro(self, command): {"X": pos.x, "Y": pos.y, "F": self.estimator.feedrate} ) command = command.replace(MACRO_CHAR + m + MACRO_CHAR, str(res)) - except Exception as e: + except Exception as exception: # TODO handle this in a better way self._logger.error("Error while parsing macro: " + m) - self._logger.error(e) + self._logger.error(exception) return command def _prepare_command(self, command): diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index 49f5606f..ea8f9aeb 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -146,14 +146,17 @@ def _generate_line(self, command, n=None): if c[0] == "N": self.line_number = int(c[1:]) - 1 self.buffer.clear() + # to set the line number just need "Nn M110" + # the correct N value will be added automatically during the command creation + line = "M110" # add checksum if n is None: n = self.line_number if self.fast_mode: - line = "N{}{}".format(n, line) + line = f"N{n}{line}" else: - line = "N{} {} ".format(n, line) + line = f"N{n} {line} " # calculate marlin checksum according to the wiki cs = 0 for i in line: From 84ba4697f6fdcbe540a06bc532307c5595fe4958 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 2 Apr 2022 13:09:23 +0200 Subject: [PATCH 34/52] Still problems with the stop function between drawings --- server/hardware/device/comunication/device_serial.py | 2 -- server/hardware/device/feeder.py | 7 +++---- server/hardware/device/firmwares/marlin.py | 2 ++ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index a193af73..e12ffac5 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -29,8 +29,6 @@ def __init__(self, serial_name=None, baudrate=115200, logger_name=None): self.baudrate = baudrate self.is_virtual = False self.serial = None - self._buffer = bytearray() - self.echo = "" self._emulator = Emulator() self._readline_buffer = ReadlineBuffer() diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index 52df3d6f..b8dffd89 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -222,10 +222,9 @@ def stop(self): break # waiting comand buffer to be cleared before calling the "drawing ended" event - # while True: - # if len(self._device.buffer) == 0: - # break - # FIXME this is not working correctly, the buffer is not getting cleared correctly for some reason (with marlin at least) + while True: + if len(self._device.buffer) == 0: + break # clean the device status self._device.reset_status() diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index ea8f9aeb..28bac7ce 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -1,5 +1,6 @@ from copy import deepcopy from threading import Timer +from time import sleep from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler from server.hardware.device.firmwares.generic_firmware import GenericFirmware @@ -185,3 +186,4 @@ def _reset_line_number(self, line_number=3): self._logger.info("Clearing buffer and resetting line number") self.buffer.clear() self.send_gcode_command(f"M110 N{line_number}") + sleep(1) From d0c7dcf2233052c6ca4f2845d6ac49ef653bb661 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 13:12:24 +0200 Subject: [PATCH 35/52] Bump minimist from 1.2.5 to 1.2.6 in /frontend (#74) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 40b26f54..5dd1ef0b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7417,9 +7417,9 @@ minimatch@3.0.4, minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass-collect@^1.0.2: version "1.0.2" From 23e353e0d03bf8a84b14acfc52a8ea455e03156b Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 10 Apr 2022 11:09:00 +0200 Subject: [PATCH 36/52] Major changes to the serial class. Now it is using the callbacks from within a new thread to avoid deadlocks between readline and send. The serial is experiencing far less errors. Still some troubles with the stop() --- .../device/comunication/device_serial.py | 22 +- server/hardware/device/feeder.py | 12 +- .../device/firmwares/commands_buffer.py | 6 + .../device/firmwares/generic_firmware.py | 44 ++-- server/hardware/device/firmwares/marlin.py | 200 +++++++++++------- 5 files changed, 182 insertions(+), 102 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index e12ffac5..ea333960 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -1,7 +1,9 @@ +from queue import Queue import sys import logging import glob from threading import Thread, RLock +from time import sleep import serial import serial.tools.list_ports @@ -37,11 +39,17 @@ def useless(arg): pass # setting up the read thread - self._th = Thread(target=self._thf, daemon=True) self._mutex = RLock() + self._th = Thread(target=self._thf, daemon=True) self._th.name = "serial_read" self._running = False + + # setting up callbacks (they are called in a separate thread to have non blocking serial handling) self.set_on_readline_callback(useless) + self._callbacks_th = Thread(target=self._use_callbacks) + self._callbacks_th.name = "serial_callbacks" + self._callbacks_th.start() + self._callbacks_queue = Queue() def open(self): """ @@ -104,10 +112,11 @@ def send(self, line): else: if self.serial.is_open: try: - while self.serial.out_waiting: - pass + while self.serial.out_waiting > 0 or (self.serial.in_waiting > 0): + sleep(0.01) with self._mutex: self.serial.write(str(line).encode()) + # TODO add the line to a queue and then send the queue somewhere else when possible? # TODO try to send byte by byte instead of a full line? # (to reduce the risk of sending commands with missing digits or wrong values # that may lead to a wrong position value) @@ -171,6 +180,13 @@ def _thf(self): full_lines = self._readline_buffer.full_lines # use the callback for every full line available for full_line in full_lines: + self._callbacks_queue.put(full_line) + + def _use_callbacks(self): + """Run the callback when a line is received""" + while True: + if not self._callbacks_queue.empty(): + full_line = self._callbacks_queue.get() try: self._on_readline(full_line) except Exception as exception: diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index b8dffd89..516ad928 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -212,7 +212,7 @@ def stop(self): tmp = ( self._current_element ) # store the current element to raise the "on_element_ended" callback - self._current_element = None + # self._current_element = None self._status.running = False if not self._stopped: self.logger.info("Stopping drawing") @@ -223,13 +223,16 @@ def stop(self): # waiting comand buffer to be cleared before calling the "drawing ended" event while True: - if len(self._device.buffer) == 0: + self.logger.info(f"Buffer length: {len(self._device.buffer)}") + time.sleep(0.1) + if len(self._device.buffer) <= 1: break # clean the device status self._device.reset_status() # call the element ended callback + self.logger.info(f"Buffer length: {len(self._device.buffer)} and calling on element ended") self.event_handler.on_element_ended(tmp) def send_gcode_command(self, command): @@ -349,6 +352,11 @@ def __thf(self): break time.sleep(0.1) + if self._stopped: + self.logger.info("Element stopped") + else: + self.logger.info("Element finished") + # run the "after" script only if the given element is a drawing with self._mutex: if isinstance(self._current_element, DrawingElement): diff --git a/server/hardware/device/firmwares/commands_buffer.py b/server/hardware/device/firmwares/commands_buffer.py index 22a18ee4..9c8868f3 100644 --- a/server/hardware/device/firmwares/commands_buffer.py +++ b/server/hardware/device/firmwares/commands_buffer.py @@ -100,8 +100,14 @@ def check_buffer_mutex_status(self): self._send_mutex.release() def popleft(self): + """Return and remove the last entry""" return self._buffer.popleft() + @property + def last(self): + """Return the last entry""" + return self._buffer[-1] + def __len__(self): with self._mutex: return len(self._buffer) diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index bceb11b7..04c7847c 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -47,7 +47,9 @@ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler) self.line_number = 0 self._command_resolution = "{:.3f}" # by default will use 3 decimals in fast mode self._mutex = RLock() - self.serial_mutex = Lock() + # this mutex is used to send messages with a priority with respect to the standard commands + # (like for the 'resend' command in marlin for which all the commands must be sent in a row) + self._priority_mutex = Lock() self._is_ready = False # the device will not be ready at the beginning self.estimator = GenericEstimator() @@ -143,12 +145,23 @@ def get_current_position(self): def send_gcode_command(self, command, hide_command=False): """ Send the command + + Args: + command [str]: the command to send + hide_command [bool]: True to hide the command from the list of sent commands in the UI """ command = self._prepare_command(command) + if command == "": + return # wait until the lock for the buffer length is released # if the lock is released means the board sent the ack for older lines and can send new ones with self._buffer.get_buffer_wait_mutex(): pass + # if the priority mutex is locked should wait before sending the command until it is unlocked + while self._priority_mutex.locked(): + ... + + # now can send the command with self._mutex: self._handle_send_command(command, hide_command) self.estimator.parse_command(command) @@ -160,14 +173,13 @@ def connect(self): """ with self._mutex: self._logger.info("Connecting to the serial device") - with self.serial_mutex: - self._serial_device = DeviceSerial( - self._serial_settings["port"]["value"], - self._serial_settings["baud"]["value"], - self._logger.name, - ) - self._serial_device.set_on_readline_callback(self._on_readline) - self._serial_device.open() + self._serial_device = DeviceSerial( + self._serial_settings["port"]["value"], + self._serial_settings["baud"]["value"], + self._logger.name, + ) + self._serial_device.set_on_readline_callback(self._on_readline) + self._serial_device.open() # wait device ready if not self._serial_device.is_connected: time.sleep(1) @@ -243,14 +255,11 @@ def _handle_send_command(self, command, hide_command=False): # send the command after parsing the content # need to use the mutex here because it is changing also the line number - with self.serial_mutex: - line = self._generate_line(command) - - self._serial_device.send(line) # send line - self._buffer.push_command(line, self.line_number) - self._logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) + line = self._generate_line(command) - # TODO fix the problem with small geometries may be with the serial port being to slow. For long (straight) segments the problem is not evident. Do not understand why it is happening + self._serial_device.send(line) # send line + self._buffer.push_command(line, self.line_number) + self._logger.log(settings_utils.LINE_SENT, line.replace("\n", "")) if not hide_command: self.event_handler.on_line_sent(line) # uses the handler callback for the new line @@ -296,8 +305,7 @@ def _on_timeout(self): command = self.force_ack_command line = self._generate_line(command) self._logger.log(settings_utils.LINE_SERVICE, line) - with self.serial_mutex: - self._serial_device.send(line) + self._serial_device.send(line) else: self._update_timeout() diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index 28bac7ce..fe6d41f3 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -1,5 +1,5 @@ from copy import deepcopy -from threading import Timer +from threading import Thread, Timer, Lock from time import sleep from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler @@ -20,6 +20,8 @@ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler) self.force_ack_command = "M114" # tolerance position (needed because the marlin rounding for the actual position is not the usual rounding) self.position_tolerance = 0.01 + # this variable is used to keep track if is already resending commands or not + self._resending_commands = Lock() def emergency_stop(self): """ @@ -47,85 +49,126 @@ def _on_readline(self, line): Returns: True if the line is handled correctly """ - with self._mutex: - # if the line is not valid will return False - if not super()._on_readline(line): - return False - - hide_line = False - # Resend - if "Error:Line Number" in line: - line_found = False - line_number = int(line.replace("\r\n", "").split(" ")[-1]) + 1 - items = deepcopy(self.buffer._buffer_history) - first_available_line = None - for command_n, command in items.items(): - n_line_number = int(command_n.strip("N")) - if n_line_number == line_number: - line_found = True - if n_line_number >= line_number: - if first_available_line is None: - first_available_line = line_number - # All the lines after the required one must be resent. Cannot break the loop now - self._serial_device.send(command) - self._logger.error( - "Line not received correctly. Resending: {}".format(command.strip("\n")) - ) - - if (not line_found) and not (first_available_line is None): - for i in range(line_number, first_available_line): - self._serial_device.send(self._generate_line(self.force_ack_command, n=i)) - - self.buffer.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: - self._logger.error( - "No line was found for the number required. Restart numeration." - ) - self._reset_line_number() - - # unknow command - elif "echo:Unknown command:" in line: - self._logger.error("Error: command not found. Can also be a communication error") - - # M114 response contains the "Count" word - # the response looks like: X:115.22 Y:116.38 Z:0.00 E:0.00 Count A:9218 B:9310 Z:0 - # still, M114 will receive the last position in the look-ahead planner thus the drawing will end first on the interface and then in the real device - elif "Count" in line: - try: - l = line.split(" ") - x = float(l[0][2:]) # remove "X:" from the string - y = float(l[1][2:]) # remove "Y:" from the string - except Exception as e: - self._logger.error("Error while parsing M114 result for line: {}".format(line)) - self._logger.exception(e) - - commanded_position = self.estimator.get_last_commanded_position() - # if the last commanded position coincides with the current position it means the buffer on the device is empty (could happen that the position is the same between different points but the M114 command should not be that frequent to run into this problem.) TODO check if it is good enough or if should implement additional checks like a timeout - # use a tolerance instead of equality because marlin is using strange rounding for the coordinates - if (abs(float(commanded_position.x) - x) < self.position_tolerance) and ( - abs(float(commanded_position.y) - y) < self.position_tolerance - ): - if not self.buffer.is_empty(): - self.buffer.ack_received() - else: - self.buffer.clear() - self.buffer.check_buffer_mutex_status() - - if not self.buffer.is_empty(): - hide_line = True - # the device send a "start" line when ready - elif "start" in line: - # adding delay otherwise there is a collision most of the time (n seconds) - Timer(2, self._on_device_ready).start() - - # 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 - - self._log_received_line(line, hide_line) + # cannot use the mutex here because the callback is used inside the serial which is also used to read back the data + # if the mutex is used, the send command will block the readline command which is prioritized + + # if the line is not valid will return False + if not super()._on_readline(line): + return False + + hide_line = False + + # Parsing the received command + # Resend + if ("Resend:" in line) or ("Error:checksum mismatch" in line): + hide_line = self._resend(line) + + # M114 response contains the "Count" word + # the response looks like: X:115.22 Y:116.38 Z:0.00 E:0.00 Count A:9218 B:9310 Z:0 + # still, M114 will receive the last position in the look-ahead planner thus the drawing will end first on the interface and then in the real device + elif "Count" in line: + hide_line = self._count(line) + + # the device send a "start" line when ready + elif "start" in line: + # adding delay otherwise there is a collision most of the time (n seconds) + Timer(2, self._on_device_ready).start() + + # unknow command + elif "echo:Unknown command:" in line: + self._logger.error("Error: command not found. Can also be a communication error") + # resend the last command sent + self._resend_command_list([self.buffer.last]) + + # 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 + + self._log_received_line(line, hide_line) return True + def _count(self, line): + """ + Synchronize the estimated position with the real buffer position + + Args: + line [str]: the line received from the device which should be using the correct format + + Returns: + True if the line must be hidden in the printout of received commands + """ + try: + l = line.split(" ") + x = float(l[0][2:]) # remove "X:" from the string + y = float(l[1][2:]) # remove "Y:" from the string + except Exception as e: + self._logger.error("Error while parsing M114 result for line: {}".format(line)) + self._logger.exception(e) + + commanded_position = self.estimator.get_last_commanded_position() + # if the last commanded position coincides with the current position it means the buffer on the device is empty (could happen that the position is the same between different points but the M114 command should not be that frequent to run into this problem.) TODO check if it is good enough or if should implement additional checks like a timeout + # use a tolerance instead of equality because marlin is using strange rounding for the coordinates + if (abs(float(commanded_position.x) - x) < self.position_tolerance) and ( + abs(float(commanded_position.y) - y) < self.position_tolerance + ): + if not self.buffer.is_empty(): + self.buffer.ack_received() + else: + self.buffer.clear() + self.buffer.check_buffer_mutex_status() + + return not self.buffer.is_empty() + + def _resend(self, line): + """ + Handle a resend command + + Args: + line [str]: the line received with the error (should contain the line number to send back) + + Returns: + True if the received line should be hidden in the printout of received commands + """ + line_found = False + # need to resend the commands outside the mutex of the feeder -> store the commands in a list and use a different thread to send them + commands_to_resend = [] + if not self._priority_mutex.locked(): + self._priority_mutex.acquire() + line_number = int(line.replace("\r\n", "").split(" ")[-1]) + self._logger.info(f"Line not received correctly. Resending from N{line_number}") + items = deepcopy(self.buffer._buffer_history) + first_available_line = None + for command_n, command in items.items(): + n_line_number = int(command_n.strip("N")) + if n_line_number == line_number: + line_found = True + if n_line_number >= line_number: + if first_available_line is None: + first_available_line = line_number + # All the lines after the required one must be resent. Cannot break the loop now + commands_to_resend.append(command) + + if len(commands_to_resend) > 1 and line_found: + th = Thread(target=self._resend_command_list, args=(commands_to_resend,), daemon=True) + th.name = "marlin_resend_commands" + th.start() + self.buffer.ack_received(safe_line_number=line_number - 1, append_left_extra=True) + else: + self._logger.error("No line was found for the number required. Restart numeration.") + self._reset_line_number() + + # if (not line_found) and not (first_available_line is None): + # for i in range(line_number, first_available_line): + # self._serial_device.send(self._generate_line(self.force_ack_command, n=i)) + + return False + + def _resend_command_list(self, commands): + with self._resending_commands: + for command in commands: + self._logger.info(f"Resending command: {command}") + self._serial_device.send(command) + def _generate_line(self, command, n=None): """ Clean the command, substitute the macro values and add checksum @@ -186,4 +229,3 @@ def _reset_line_number(self, line_number=3): self._logger.info("Clearing buffer and resetting line number") self.buffer.clear() self.send_gcode_command(f"M110 N{line_number}") - sleep(1) From c693e6e6c3f80292be608f7e9b11935ab4ad822c Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 10 Apr 2022 11:42:35 +0200 Subject: [PATCH 37/52] Looks like the issue with the stop is fixed. Still some troubles with some resend commands --- .../device/comunication/device_serial.py | 6 ++++- server/hardware/device/feeder.py | 8 +++--- .../device/firmwares/generic_firmware.py | 2 ++ server/hardware/device/firmwares/marlin.py | 21 +++++----------- server/preprocessing/file_observer.py | 25 ++++++++++++++----- 5 files changed, 36 insertions(+), 26 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index ea333960..94d425d3 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -183,7 +183,11 @@ def _thf(self): self._callbacks_queue.put(full_line) def _use_callbacks(self): - """Run the callback when a line is received""" + """ + Run the callback when a line is received + + Keep the operation asynchronous to avoid deadlocks with the "send" command + """ while True: if not self._callbacks_queue.empty(): full_line = self._callbacks_queue.get() diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index 516ad928..f3920edd 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -223,16 +223,16 @@ def stop(self): # waiting comand buffer to be cleared before calling the "drawing ended" event while True: - self.logger.info(f"Buffer length: {len(self._device.buffer)}") + self.logger.debug(f"Stopping element. Buffer length: {len(self._device.buffer)}") time.sleep(0.1) - if len(self._device.buffer) <= 1: + if len(self._device.buffer) == 0: break # clean the device status self._device.reset_status() # call the element ended callback - self.logger.info(f"Buffer length: {len(self._device.buffer)} and calling on element ended") + self.logger.info(f"Calling on element ended") self.event_handler.on_element_ended(tmp) def send_gcode_command(self, command): @@ -350,7 +350,7 @@ def __thf(self): # if not paused or if a stop command is used should exit the loop if not self._status.paused or not self._status.running: break - time.sleep(0.1) + time.sleep(0.5) if self._stopped: self.logger.info("Element stopped") diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index 04c7847c..46e4b21b 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -208,6 +208,8 @@ def reset_status(self): """ with self._mutex: self.line_number = 0 + if self._priority_mutex.locked(): + self._priority_mutex.release() def _parse_macro(self, command): """ diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index fe6d41f3..84d9ded2 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -1,6 +1,5 @@ from copy import deepcopy -from threading import Thread, Timer, Lock -from time import sleep +from threading import Timer, Lock from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler from server.hardware.device.firmwares.generic_firmware import GenericFirmware @@ -78,7 +77,7 @@ def _on_readline(self, line): elif "echo:Unknown command:" in line: self._logger.error("Error: command not found. Can also be a communication error") # resend the last command sent - self._resend_command_list([self.buffer.last]) + self._serial_device.send(self.buffer.last) # TODO check feedrate response for M220 and set feedrate # elif "_______" in line: # must see the real output from marlin @@ -146,16 +145,14 @@ def _resend(self, line): if first_available_line is None: first_available_line = line_number # All the lines after the required one must be resent. Cannot break the loop now - commands_to_resend.append(command) + self._serial_device.send(command) - if len(commands_to_resend) > 1 and line_found: - th = Thread(target=self._resend_command_list, args=(commands_to_resend,), daemon=True) - th.name = "marlin_resend_commands" - th.start() + if line_found: self.buffer.ack_received(safe_line_number=line_number - 1, append_left_extra=True) else: self._logger.error("No line was found for the number required. Restart numeration.") - self._reset_line_number() + # will reset the buffer and restart the numeration + self.reset_status() # if (not line_found) and not (first_available_line is None): # for i in range(line_number, first_available_line): @@ -163,12 +160,6 @@ def _resend(self, line): return False - def _resend_command_list(self, commands): - with self._resending_commands: - for command in commands: - self._logger.info(f"Resending command: {command}") - self._serial_device.send(command) - def _generate_line(self, command, n=None): """ Clean the command, substitute the macro values and add checksum diff --git a/server/preprocessing/file_observer.py b/server/preprocessing/file_observer.py index c7d7f28e..c7bdae55 100644 --- a/server/preprocessing/file_observer.py +++ b/server/preprocessing/file_observer.py @@ -11,6 +11,10 @@ class GcodeObserverManager: + """ + This class is used to observe a folder and upload every gcode file that is found inside it + """ + def __init__(self, path=".", logger=None): if logger is None: logger_name = __name__ @@ -25,13 +29,16 @@ def __init__(self, path=".", logger=None): self.check_current_files() def start(self): + """Start the observer""" self._observer.start() def stop(self): + """Stop the observer""" self._observer.stop() self._observer.join() def check_current_files(self): + """Check the files that are in the folder""" files = fnmatch.filter(os.listdir(self._path), "*.gcode") if len(files) > 0: self._logger.info("Found some files to load in the autodetect folder") @@ -40,20 +47,26 @@ def check_current_files(self): class GcodeEventHandler(PatternMatchingEventHandler): + """Handle the file once is detected by the observer""" + def __init__(self, logger): super().__init__(patterns=["*.gcode"]) self._logger = logger - def on_created(self, evt): - self.handle_event(evt) + def on_created(self, event): + """Handle the creation of a new file""" + self.handle_event(event) - def on_moved(self, evt): - self.handle_event(evt) + def on_moved(self, event): + """Handle the copy of file inside the folder""" + self.handle_event(event) - def handle_event(self, evt): - self.init_drawing(evt.src_path) + def handle_event(self, event): + """Handle a generic event""" + self.init_drawing(event.src_path) def init_drawing(self, filename): + """Save the file in the database and create the preview""" self._logger.info("Uploading autodetected file: {}".format(filename)) try: id = "" From 4979584c9ef4178825225a47cdcde500df67d769 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 10 Apr 2022 11:55:56 +0200 Subject: [PATCH 38/52] Fixing some comments --- server/database/models.py | 1 - server/hardware/device/comunication/device_serial.py | 5 +---- server/hardware/device/feeder.py | 4 +--- server/hardware/leds/light_sensors/generic_light_sensor.py | 1 - server/hardware/leds/light_sensors/tsl2591.py | 2 -- 5 files changed, 2 insertions(+), 11 deletions(-) diff --git a/server/database/models.py b/server/database/models.py index 7f7f35f9..e8eed135 100644 --- a/server/database/models.py +++ b/server/database/models.py @@ -168,7 +168,6 @@ def get_playlist(cls, id): raise ValueError("An id is necessary to select a playlist") try: return db.session.query(Playlists).filter(Playlists.id == id).one() - # TODO check if there is at least one line (if the playlist exist) except: return Playlists.create_playlist() diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 94d425d3..03d4b627 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -112,14 +112,11 @@ def send(self, line): else: if self.serial.is_open: try: + # wait for the serial to be clear before sending to reduce the possibility of a collision while self.serial.out_waiting > 0 or (self.serial.in_waiting > 0): sleep(0.01) with self._mutex: self.serial.write(str(line).encode()) - # TODO add the line to a queue and then send the queue somewhere else when possible? - # TODO try to send byte by byte instead of a full line? - # (to reduce the risk of sending commands with missing digits or wrong values - # that may lead to a wrong position value) except: self.close() self.logger.error("Error while sending a command") diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index f3920edd..d0da2c6e 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -38,7 +38,6 @@ class Feeder(FirwmareEventHandler): Handle single commands but also the preloaded scripts and complete drawings or elements """ - # FIXME remove the None default from the event handler def __init__(self, event_handler: FeederEventHandler): """ Args: @@ -203,7 +202,6 @@ def stop(self): This is a blocking function. Will wait until the element is completely stopped before going on with the execution """ - # TODO: make it non blocking since the even is called when the drawing is stopped with self._mutex: # if is not running, no need to stop it if not self._status.running: @@ -232,7 +230,7 @@ def stop(self): self._device.reset_status() # call the element ended callback - self.logger.info(f"Calling on element ended") + self.logger.info("Calling on element ended") self.event_handler.on_element_ended(tmp) def send_gcode_command(self, command): diff --git a/server/hardware/leds/light_sensors/generic_light_sensor.py b/server/hardware/leds/light_sensors/generic_light_sensor.py index bfe2d4a1..22e009df 100644 --- a/server/hardware/leds/light_sensors/generic_light_sensor.py +++ b/server/hardware/leds/light_sensors/generic_light_sensor.py @@ -38,7 +38,6 @@ def _thf(self): self._history.append(brightness) brightness = sum(self._history) / float(len(self._history)) - self.app.logger.info("Averaged brightness: {}".format(brightness)) # FIXME remove this self.app.lmanager.set_brightness(brightness) self.app.lmanager.set_brightness(1) diff --git a/server/hardware/leds/light_sensors/tsl2591.py b/server/hardware/leds/light_sensors/tsl2591.py index 25229c93..31bc1393 100644 --- a/server/hardware/leds/light_sensors/tsl2591.py +++ b/server/hardware/leds/light_sensors/tsl2591.py @@ -24,8 +24,6 @@ def get_brightness(self): tmp = max( sqrt(min(lux, LUX_MAX) / LUX_MAX), BRIGHTNESS_MIN ) # calculating the brightness to use - self.app.logger.info("Sensor light intensity: {} lux".format(lux)) # FIXME remove this - self.app.logger.info("Sensor current brightness: {}".format(tmp)) # FIXME remove this return tmp @property From 255dd99bc07d78d13908cacd97b36b3408f7aef1 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 10 Apr 2022 14:00:33 +0200 Subject: [PATCH 39/52] Fixing issue with emulation: the software was not starting correctly. Fixed by putting a smll pause in the serial device loops --- .../device/comunication/device_serial.py | 28 +++++++++++++------ .../hardware/device/comunication/emulator.py | 3 +- server/hardware/device/feeder.py | 4 +-- .../device/firmwares/generic_firmware.py | 9 +++--- server/hardware/device/firmwares/marlin.py | 1 - server/hardware/leds/leds_controller.py | 1 + server/tests/test_z_firmwares.py | 8 +++--- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 03d4b627..759c1364 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -10,6 +10,9 @@ from server.hardware.device.comunication.emulator import Emulator from server.hardware.device.comunication.readline_buffer import ReadlineBuffer +# loops in this class need a short sleep otherwise the entire app get stuck for some reason +LOOPS_SLEEP_TIME = 0.001 + class DeviceSerial: """ @@ -45,11 +48,11 @@ def useless(arg): self._running = False # setting up callbacks (they are called in a separate thread to have non blocking serial handling) + self._callbacks_queue = Queue() self.set_on_readline_callback(useless) self._callbacks_th = Thread(target=self._use_callbacks) self._callbacks_th.name = "serial_callbacks" self._callbacks_th.start() - self._callbacks_queue = Queue() def open(self): """ @@ -58,18 +61,24 @@ def open(self): If the port is not working, work as a virtual device """ 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") + if self.serialname in self.get_serial_port_list(): + 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") + else: + self.is_virtual = True + self.logger.error( + "The selected serial port is not available. Starting a virtual device..." + ) except Exception as e: # FIXME should check for different exceptions self.logger.exception(e) self.is_virtual = True self.logger.error( - "Serial not available. Are you sure the device is connected and is not in use by other softwares? \ - (Will use the virtual serial)" + "Serial not available. Are you sure the device is connected and is not in use by other softwares? " + + "(Will use the virtual serial)" ) self._th.start() @@ -114,7 +123,7 @@ def send(self, line): try: # wait for the serial to be clear before sending to reduce the possibility of a collision while self.serial.out_waiting > 0 or (self.serial.in_waiting > 0): - sleep(0.01) + sleep(LOOPS_SLEEP_TIME) with self._mutex: self.serial.write(str(line).encode()) except: @@ -186,6 +195,7 @@ def _use_callbacks(self): Keep the operation asynchronous to avoid deadlocks with the "send" command """ while True: + sleep(LOOPS_SLEEP_TIME) if not self._callbacks_queue.empty(): full_line = self._callbacks_queue.get() try: diff --git a/server/hardware/device/comunication/emulator.py b/server/hardware/device/comunication/emulator.py index 14707a89..c3753cad 100644 --- a/server/hardware/device/comunication/emulator.py +++ b/server/hardware/device/comunication/emulator.py @@ -7,7 +7,7 @@ emulated_commands_with_delay = ["G0", "G00", "G1", "G01"] -ACK = "ok\n\r" +ACK = "ok\n" class Emulator: @@ -98,6 +98,7 @@ def readline(self): """ Readline method for the emulated device. Used by the serial controller """ + # this time is needed to slow down the loop otherwise the software get stuck with the emulator time.sleep(0.001) # special commands response if len(self.message_buffer) >= 1: diff --git a/server/hardware/device/feeder.py b/server/hardware/device/feeder.py index d0da2c6e..f0a773e5 100644 --- a/server/hardware/device/feeder.py +++ b/server/hardware/device/feeder.py @@ -223,7 +223,7 @@ def stop(self): while True: self.logger.debug(f"Stopping element. Buffer length: {len(self._device.buffer)}") time.sleep(0.1) - if len(self._device.buffer) == 0: + if len(self._device.buffer) <= 1: break # clean the device status @@ -348,7 +348,7 @@ def __thf(self): # if not paused or if a stop command is used should exit the loop if not self._status.paused or not self._status.running: break - time.sleep(0.5) + time.sleep(0.1) if self._stopped: self.logger.info("Element stopped") diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index 46e4b21b..c06dfec8 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -1,7 +1,7 @@ import logging import re -from threading import RLock, Lock +from threading import RLock, Lock, Timer import time from py_expression_eval import Parser from server.hardware.device.comunication.device_serial import DeviceSerial @@ -181,9 +181,9 @@ def connect(self): self._serial_device.set_on_readline_callback(self._on_readline) self._serial_device.open() # wait device ready - if not self._serial_device.is_connected: - time.sleep(1) - self._on_device_ready() + if not self._serial_device.is_connected: + # calling the "device ready" callback with a delay + Timer(6, self._on_device_ready).start() def close(self): """ @@ -325,7 +325,6 @@ def _on_readline(self, line): Returns: True if the readline has done correctly """ - # with self._mutex: if line is None: return False if self.ack in line: diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index 84d9ded2..dced149d 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -130,7 +130,6 @@ def _resend(self, line): """ line_found = False # need to resend the commands outside the mutex of the feeder -> store the commands in a list and use a different thread to send them - commands_to_resend = [] if not self._priority_mutex.locked(): self._priority_mutex.acquire() line_number = int(line.replace("\r\n", "").split(" ")[-1]) diff --git a/server/hardware/leds/leds_controller.py b/server/hardware/leds/leds_controller.py index 4ad33abe..955e7af4 100644 --- a/server/hardware/leds/leds_controller.py +++ b/server/hardware/leds/leds_controller.py @@ -156,6 +156,7 @@ def update_settings(self, settings): self.app.semits.show_toast_on_UI("Led driver type not compatible with current HW") self.app.logger.exception(e) self.app.logger.error("Cannot initialize leds controller") + return try: if settings.leds.light_sensor == "TSL2591": self.sensor = TSL2591(self.app) diff --git a/server/tests/test_z_firmwares.py b/server/tests/test_z_firmwares.py index a61c70a6..88b98ca5 100644 --- a/server/tests/test_z_firmwares.py +++ b/server/tests/test_z_firmwares.py @@ -28,10 +28,10 @@ def on_device_ready(self): def run_test(device, fast_mode=False): device.connect() device.fast_mode = fast_mode - device.send_gcode_command("G28") - device.send_gcode_command("G0 X0 Y0 F3000") - for x in range(15): - device.send_gcode_command(f"G0 X{x} Y0") + # device.send_gcode_command("G28") + # device.send_gcode_command("G0 X0 Y0 F3000") + # for x in range(15): + # device.send_gcode_command(f"G0 X{x} Y0") return True From c60c1a6d3c6f90a50ff897e9d3a78a799c9aa652 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sun, 10 Apr 2022 14:15:34 +0200 Subject: [PATCH 40/52] Fixing endless loop in serial device for tests --- server/hardware/device/comunication/device_serial.py | 5 +++-- server/hardware/device/firmwares/generic_firmware.py | 7 ++++--- server/hardware/device/firmwares/marlin.py | 4 +++- server/tests/conftest.py | 11 ++++++----- server/tests/test_z_firmwares.py | 12 ++++++++---- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 759c1364..7aa26696 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -52,7 +52,6 @@ def useless(arg): self.set_on_readline_callback(useless) self._callbacks_th = Thread(target=self._use_callbacks) self._callbacks_th.name = "serial_callbacks" - self._callbacks_th.start() def open(self): """ @@ -177,6 +176,8 @@ def _thf(self): """ self._running = True + self._callbacks_th.start() + while self.is_running: # do not understand why but with the emulator need this to make everything work correctly with self._mutex: @@ -194,7 +195,7 @@ def _use_callbacks(self): Keep the operation asynchronous to avoid deadlocks with the "send" command """ - while True: + while self._running: sleep(LOOPS_SLEEP_TIME) if not self._callbacks_queue.empty(): full_line = self._callbacks_queue.get() diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index c06dfec8..cd9974d8 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -2,10 +2,9 @@ import re from threading import RLock, Lock, Timer -import time from py_expression_eval import Parser -from server.hardware.device.comunication.device_serial import DeviceSerial +from server.hardware.device.comunication.device_serial import DeviceSerial from server.hardware.device.estimation.generic_estimator import GenericEstimator from server.hardware.device.firmwares.commands_buffer import CommandBuffer from server.hardware.device.firmwares.firmware_event_handler import FirwmareEventHandler @@ -183,7 +182,9 @@ def connect(self): # wait device ready if not self._serial_device.is_connected: # calling the "device ready" callback with a delay - Timer(6, self._on_device_ready).start() + timer = Timer(2, self._on_device_ready) + timer.daemon = True + timer.start() def close(self): """ diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index dced149d..d11b69ba 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -71,7 +71,9 @@ def _on_readline(self, line): # the device send a "start" line when ready elif "start" in line: # adding delay otherwise there is a collision most of the time (n seconds) - Timer(2, self._on_device_ready).start() + timer = Timer(2, self._on_device_ready) + timer.daemon = True + timer.start() # unknow command elif "echo:Unknown command:" in line: diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 8e595d5e..0cdf9cf9 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -18,16 +18,17 @@ @pytest.fixture(scope="session") def client(): db_fd, db_fu = tempfile.mkstemp() - server.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' - server.app.config['TESTING'] = True + server.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" + server.app.config["TESTING"] = True with server.app.test_client() as client: with server.app.app_context(): - db.create_all() + db.create_all() yield client db.drop_all() + # stopping feeder + server.app.feeder._device.close() + os.close(db_fd) os.unlink(db_fu) - - diff --git a/server/tests/test_z_firmwares.py b/server/tests/test_z_firmwares.py index 88b98ca5..53fc9608 100644 --- a/server/tests/test_z_firmwares.py +++ b/server/tests/test_z_firmwares.py @@ -3,6 +3,7 @@ """ import logging +from time import sleep from server.hardware.device.firmwares.grbl import Grbl from server.hardware.device.firmwares.marlin import Marlin @@ -28,10 +29,11 @@ def on_device_ready(self): def run_test(device, fast_mode=False): device.connect() device.fast_mode = fast_mode - # device.send_gcode_command("G28") - # device.send_gcode_command("G0 X0 Y0 F3000") - # for x in range(15): - # device.send_gcode_command(f"G0 X{x} Y0") + device.send_gcode_command("G0 X0 Y0 F3000") + for x in range(15): + device.send_gcode_command(f"G0 X{x} Y0") + + device.close() return True @@ -47,6 +49,7 @@ def test_marlin(): Marlin(serial_settings=settings, logger=logger_name, event_handler=EventHandler()), fast_mode=True, ) + sleep(3) def test_grbl(): @@ -60,3 +63,4 @@ def test_grbl(): Grbl(serial_settings=settings, logger=logger_name, event_handler=EventHandler()), fast_mode=True, ) + sleep(3) From 8901129e77ec46447cb358d06cfd591cba3e224d Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Fri, 15 Apr 2022 11:03:55 +0200 Subject: [PATCH 41/52] Flask upgrade now is stuck and do not stop automatically. Need to find a solution --- server/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/__init__.py b/server/__init__.py index bc6e3ce1..59c33b28 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -140,6 +140,12 @@ def home(): return send_from_directory(app.static_folder, "index.html") +@app.teardown_appcontext +def shutdown_session(exception=None): + db.session.close() + db.engine.dispose() + + # Starting the feeder after the server is ready to avoid problems with the web page not showing up def run_post(): sleep(2) From 7bbd06bd61426c5516966aa64f35a0053a6a4d8b Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Fri, 15 Apr 2022 11:57:25 +0200 Subject: [PATCH 42/52] Adding git attributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..94f480de --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file From 20bf144866ffc2986100f3e9f8aedc1195a2e224 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Fri, 15 Apr 2022 12:10:25 +0200 Subject: [PATCH 43/52] Setting all threads to be daemon. Now the software should be killable during the flask upgrade command --- server/__init__.py | 2 +- server/hardware/device/comunication/device_serial.py | 2 +- server/hardware/leds/light_sensors/generic_light_sensor.py | 2 +- server/utils/stats.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index 59c33b28..b2970c5e 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -152,7 +152,7 @@ def run_post(): app.lmanager.start() -th = Thread(target=run_post) +th = Thread(target=run_post, daemon=True) th.name = "feeder_starter" th.start() diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 7aa26696..81e9d49c 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -50,7 +50,7 @@ def useless(arg): # setting up callbacks (they are called in a separate thread to have non blocking serial handling) self._callbacks_queue = Queue() self.set_on_readline_callback(useless) - self._callbacks_th = Thread(target=self._use_callbacks) + self._callbacks_th = Thread(target=self._use_callbacks, daemon=True) self._callbacks_th.name = "serial_callbacks" def open(self): diff --git a/server/hardware/leds/light_sensors/generic_light_sensor.py b/server/hardware/leds/light_sensors/generic_light_sensor.py index 22e009df..4be5902e 100644 --- a/server/hardware/leds/light_sensors/generic_light_sensor.py +++ b/server/hardware/leds/light_sensors/generic_light_sensor.py @@ -19,7 +19,7 @@ def start(self): When the light sensor is started, will control the brightness of the LEDs automatically. Will change it according to the last given color (can only dim)""" self._is_running = True - self._th = Thread(target=self._thf) + self._th = Thread(target=self._thf, daemon=True) self._th.name = "light_sensor" self._th.start() diff --git a/server/utils/stats.py b/server/utils/stats.py index 4379ede5..d76abe97 100644 --- a/server/utils/stats.py +++ b/server/utils/stats.py @@ -34,9 +34,8 @@ def __init__(self): self.stats["last_on"] = time() self.start_time = 0 self._mutex = Lock() - self._th = Thread(target=self._thf) + self._th = Thread(target=self._thf, daemon=True) self._th.name = "stats_manager" - self._th.daemon = True self._th.start() def drawing_started(self): From 0005cca0683e5767f7c9492abcc44f51e299d1be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Apr 2022 13:04:01 +0200 Subject: [PATCH 44/52] Bump minimist from 1.2.5 to 1.2.6 in /frontend (#76) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: Luca Tessaro Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 5b6154c7a2181404d759360ca16ef6cf553b495b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 10:46:22 +0200 Subject: [PATCH 45/52] Bump eventsource from 1.1.0 to 1.1.1 in /frontend (#78) Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: eventsource dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: Luca Tessaro Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Luca Tessaro --- frontend/yarn.lock | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 5dd1ef0b..74234e6d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4840,9 +4840,9 @@ events@^3.0.0: integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" - integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== + version "1.1.1" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.1.tgz#4544a35a57d7120fba4fa4c86cb4023b2c09df2f" + integrity sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA== dependencies: original "^1.0.0" @@ -11215,7 +11215,7 @@ url-loader@4.1.1: mime-types "^2.1.27" schema-utils "^3.0.0" -url-parse@^1.4.3, url-parse@^1.5.3: +url-parse@^1.4.3: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -11223,6 +11223,14 @@ url-parse@^1.4.3, url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +url-parse@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" From e672dfd3a5367e8410a299b1df29fdd719605fc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 10:46:55 +0200 Subject: [PATCH 46/52] Bump async from 2.6.3 to 2.6.4 in /frontend (#77) Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: Luca Tessaro Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 74234e6d..efe857b3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2528,9 +2528,9 @@ async-limiter@~1.0.0: integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== dependencies: lodash "^4.17.14" From 12955804cd35895ef77ec59448e04ff1ce5728c7 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Wed, 6 Jul 2022 10:44:42 +0200 Subject: [PATCH 47/52] Adding a download 'diagnostic data' button --- docs/troubleshooting.md | 1 + .../tabs/settings/SoftwareVersion.js | 19 ++++-- server/__init__.py | 27 +++++--- server/static/.gitignore | 5 ++ server/utils/diagnostic.py | 61 +++++++++++++++++++ 5 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 server/static/.gitignore create mode 100644 server/utils/diagnostic.py diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4ad29a93..80de8c75 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -24,3 +24,4 @@ Please specify: * firmware (Marlin, Grbl, ...) * The issue * add a copy of the terminal result you get during the installation and when after running the software in orded to analyze better the problem +* if the UI is working, you can download a diagnostic zip file that can be uploaded together with the issue. This can be done with the button located at the bottom of the settings page. diff --git a/frontend/src/structure/tabs/settings/SoftwareVersion.js b/frontend/src/structure/tabs/settings/SoftwareVersion.js index 78ba4680..d9d03796 100644 --- a/frontend/src/structure/tabs/settings/SoftwareVersion.js +++ b/frontend/src/structure/tabs/settings/SoftwareVersion.js @@ -1,13 +1,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Col, Container, Row } from 'react-bootstrap'; -import { CloudArrowDown, CloudSlash, ExclamationTriangleFill } from 'react-bootstrap-icons'; +import { CloudArrowDown, CloudSlash, ExclamationTriangleFill, FileEarmarkArrowDown } from 'react-bootstrap-icons'; import IconButton from '../../../components/IconButton'; import { getCurrentHash, updateAutoEnabled, updateDockerComposeLatest } from './selector'; import { setTab } from '../Tabs.slice'; import { toggleAutoUpdateEnabled } from '../../../sockets/sEmits'; -import { home_site } from '../../../utils/utils'; +import { domain, home_site } from '../../../utils/utils'; const mapStateToProps = (state) => { return { @@ -27,8 +27,8 @@ class SoftwareVersion extends Component { renderUpdateButton() { if (this.props.updateEnabled) - return Disable automatic updates - else return Enable automatic updates + return Disable automatic updates + else return Enable automatic updates } renderDockerComposeUpdate() { @@ -48,12 +48,19 @@ class SoftwareVersion extends Component { return

- + Current software version shash:  

{this.props.currentHash}

- + {this.renderUpdateButton()} + + window.open(domain + '/diagnostics')}> + Download diagnostic files + +
{this.renderDockerComposeUpdate()}
diff --git a/server/__init__.py b/server/__init__.py index b2970c5e..6da06824 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -1,3 +1,10 @@ +import os +import logging +from threading import Thread +from time import sleep + +from dotenv import load_dotenv + from flask import Flask, url_for from flask.helpers import send_from_directory from flask_socketio import SocketIO @@ -8,14 +15,8 @@ from werkzeug.utils import secure_filename -import os - -from time import sleep -from dotenv import load_dotenv -import logging -from threading import Thread - from server.utils import settings_utils, software_updates, migrations +from server.utils.diagnostic import generate_diagnostic_zip from server.utils.logging_utils import server_stream_handler, server_file_handler @@ -64,6 +65,9 @@ @app.route("/Drawings/") def base_static(filename): + """ + Send back the required drawing preview + """ filename = secure_filename(filename) return send_from_directory( app.root_path @@ -73,6 +77,15 @@ def base_static(filename): ) +@app.route("/diagnostics") +def download_diagnostics(): + """ + Route to download the diagnostics zip file + """ + zip_path = generate_diagnostic_zip() + return send_from_directory("static", os.path.basename(zip_path)) + + # database DATABASE_FILENAME = os.path.join("server", "database", "db", "database.db") dbpath = os.environ.get("DB_PATH") diff --git a/server/static/.gitignore b/server/static/.gitignore new file mode 100644 index 00000000..d4ffee2c --- /dev/null +++ b/server/static/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore +!placeholder.jpg \ No newline at end of file diff --git a/server/utils/diagnostic.py b/server/utils/diagnostic.py new file mode 100644 index 00000000..32b43727 --- /dev/null +++ b/server/utils/diagnostic.py @@ -0,0 +1,61 @@ +from zipfile import ZipFile +from datetime import datetime + +from os import path, walk, listdir, remove + +ZIP_PREFIX = "diagnostic_" +DIAGNOSTIC_FILE_PATH = path.join("server", "static") + + +def generate_diagnostic_zip(): + """ + Create a zip file containing log files and saved settings + """ + # remove older zip file if present + for f in listdir(DIAGNOSTIC_FILE_PATH): + if f.startswith(ZIP_PREFIX) and f.endswith(".zip"): + remove(path.join(DIAGNOSTIC_FILE_PATH, f)) + + # create the new zip file + current_date = datetime.today() + str_datetime = current_date.strftime("%Y%m%d_%H%M%S") + zip_path = path.join(DIAGNOSTIC_FILE_PATH, f"{ZIP_PREFIX}{str_datetime}.zip") + + with ZipFile(zip_path, "w") as zip_file: + # fille the zip file with the diagnostic files + for p in get_diagnostic_paths(): + # if the path is directory, add all the files within that folder + if path.isdir(p): + filenames = next(walk(p))[2] + for f in filenames: + file = path.join(p, f) + if validate_diagnostic_file(file): + zip_file.write(file) + # otherwise add the single file + else: + if validate_diagnostic_file(p): + zip_file.write(p) + return zip_path + + +def validate_diagnostic_file(filename): + """ + Filter out the files that are not necessary for the diagnostics + + Returns: True if the given file can be put inside the diagnostics zip + """ + # ignore filenames starting with a "." (hidden files) and the older zip + name = path.split(filename)[-1] + return not name.startswith(".") and not name.startswith("diagnostic_") + + +def get_diagnostic_paths(): + """ + Returns: all the paths of data that should be saved inside the diagnostics zip + """ + return [path.join("server", "saves", "saved_settings.json"), path.join("server", "logs")] + + +if __name__ == "__main__": + print(get_diagnostic_paths()) + generate_diagnostic_zip() From f2a19ceeb73c455a6342e4786f79a484be384d13 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 16 Jul 2022 13:58:43 +0200 Subject: [PATCH 48/52] Now Marlin is working better. The resend command is working but still have a lot of troubles with the serial --- server/__init__.py | 12 ++-- .../device/comunication/device_serial.py | 59 ++++++++++++++----- .../device/estimation/generic_estimator.py | 1 + .../device/firmwares/commands_buffer.py | 4 +- server/hardware/device/firmwares/marlin.py | 20 +++++-- 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/server/__init__.py b/server/__init__.py index 6da06824..fcc5417c 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -63,6 +63,12 @@ CORS(app) # setting up cors for react +# Home routes +@app.route("/") +def home(): + return send_from_directory(app.static_folder, "index.html") + + @app.route("/Drawings/") def base_static(filename): """ @@ -147,12 +153,6 @@ def versioned_url_for(endpoint, **values): return url_for(endpoint, **values) -# Home routes -@app.route("/") -def home(): - return send_from_directory(app.static_folder, "index.html") - - @app.teardown_appcontext def shutdown_session(exception=None): db.session.close() diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 81e9d49c..86da38d2 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -36,6 +36,7 @@ def __init__(self, serial_name=None, baudrate=115200, logger_name=None): self.serial = None self._emulator = Emulator() self._readline_buffer = ReadlineBuffer() + self._send_buffer = [] # empty callback function def useless(arg): @@ -102,6 +103,14 @@ def is_running(self): """ return self._running + @property + def is_send_buffer_empty(self): + """ + Returns: + True if the send_buffer is empty + """ + return not bool(self._send_buffer) + def stop(self): """ Stop the serial read thread @@ -115,19 +124,8 @@ def send(self, line): Args: line: the line to send to the device """ - if self.is_virtual: - self._emulator.send(line) - else: - if self.serial.is_open: - try: - # wait for the serial to be clear before sending to reduce the possibility of a collision - while self.serial.out_waiting > 0 or (self.serial.in_waiting > 0): - sleep(LOOPS_SLEEP_TIME) - with self._mutex: - self.serial.write(str(line).encode()) - except: - self.close() - self.logger.error("Error while sending a command") + with self._mutex: + self._send_buffer.append(line) @property def is_connected(self): @@ -158,7 +156,7 @@ def _readline(self): """ line = "" if not self.is_virtual: - if self.serial.is_open: + if self.serial.is_open and not (self.serial is None): if self.serial.in_waiting > 0: line = self.serial.readline() line = line.decode(encoding="UTF-8") @@ -170,6 +168,26 @@ def _readline(self): self._readline_buffer.update_buffer(line) + def _send_line(self): + # send a new line from the buffer + if len(self._send_buffer) == 0: + return + if self.is_virtual: + line = self._send_buffer.pop(0) + self._emulator.send(line) + else: + if self.serial.is_open: + try: + # wait for the serial to be clear before sending to reduce the possibility of a collision + if (self.serial.out_waiting > 0) or (self.serial.in_waiting > 0): + return + with self._mutex: + line = self._send_buffer.pop(0) + self.serial.write(str(line).encode()) + except: + self.close() + self.logger.error("Error while sending a command") + def _thf(self): """ Thread function for the readline @@ -182,6 +200,7 @@ def _thf(self): # do not understand why but with the emulator need this to make everything work correctly with self._mutex: self._readline() + self._send_line() # check if should use the callback when there is a new full line full_lines = self._readline_buffer.full_lines @@ -189,6 +208,18 @@ def _thf(self): for full_line in full_lines: self._callbacks_queue.put(full_line) + def filter_callbacks_queue(self, filter_fun): + """ + Remove the unprocessed strings received with the given content + + Args: + filter_fun: funtion to filter if the content should be dropped + """ + tmp = [i for i in self._callbacks_queue.queue if filter_fun(i)] + self._callbacks_queue = Queue() + for i in tmp: + self._callbacks_queue.put(i) + def _use_callbacks(self): """ Run the callback when a line is received diff --git a/server/hardware/device/estimation/generic_estimator.py b/server/hardware/device/estimation/generic_estimator.py index 3c157dc2..4318bc5c 100644 --- a/server/hardware/device/estimation/generic_estimator.py +++ b/server/hardware/device/estimation/generic_estimator.py @@ -79,6 +79,7 @@ def parse_command(self, command): if "G28" in command and (not "X" in command or "Y" in command): self._position.x = 0 self._position.y = 0 + return # G92 is handled in the buffered commands if any(code in command for code in KNOWN_COMMANDS): diff --git a/server/hardware/device/firmwares/commands_buffer.py b/server/hardware/device/firmwares/commands_buffer.py index 9c8868f3..a0b916a2 100644 --- a/server/hardware/device/firmwares/commands_buffer.py +++ b/server/hardware/device/firmwares/commands_buffer.py @@ -106,7 +106,9 @@ def popleft(self): @property def last(self): """Return the last entry""" - return self._buffer[-1] + if len(self._buffer) > 0: + return self._buffer[-1] + return None def __len__(self): with self._mutex: diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index d11b69ba..efbabc3f 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -59,8 +59,10 @@ def _on_readline(self, line): # Parsing the received command # Resend - if ("Resend:" in line) or ("Error:checksum mismatch" in line): - hide_line = self._resend(line) + # This is used both when the checksum mismatch and also when the line number is wrong + if "Resend:" in line: + line_number = int(line.replace("\r", "").replace("\n", "").split(" ")[-1]) + hide_line = self._resend(line_number) # M114 response contains the "Count" word # the response looks like: X:115.22 Y:116.38 Z:0.00 E:0.00 Count A:9218 B:9310 Z:0 @@ -120,7 +122,7 @@ def _count(self, line): return not self.buffer.is_empty() - def _resend(self, line): + def _resend(self, line_number): """ Handle a resend command @@ -134,10 +136,11 @@ def _resend(self, line): # need to resend the commands outside the mutex of the feeder -> store the commands in a list and use a different thread to send them if not self._priority_mutex.locked(): self._priority_mutex.acquire() - line_number = int(line.replace("\r\n", "").split(" ")[-1]) self._logger.info(f"Line not received correctly. Resending from N{line_number}") items = deepcopy(self.buffer._buffer_history) first_available_line = None + + # resend the commands for command_n, command in items.items(): n_line_number = int(command_n.strip("N")) if n_line_number == line_number: @@ -148,13 +151,20 @@ def _resend(self, line): # All the lines after the required one must be resent. Cannot break the loop now self._serial_device.send(command) + # clear the serial queue from other "resend callbacks" + self._serial_device.filter_callbacks_queue( + lambda x: not ("Resend:" in x or "Error:Line Number" in x) + ) + if line_found: - self.buffer.ack_received(safe_line_number=line_number - 1, append_left_extra=True) + self.buffer.ack_received(safe_line_number=line_number - 1) else: self._logger.error("No line was found for the number required. Restart numeration.") # will reset the buffer and restart the numeration self.reset_status() + self._priority_mutex.release() + # if (not line_found) and not (first_available_line is None): # for i in range(line_number, first_available_line): # self._serial_device.send(self._generate_line(self.force_ack_command, n=i)) From 8b9b88adc3cb30fb29cb1a6d9349490572353ed4 Mon Sep 17 00:00:00 2001 From: Luca Tessaro Date: Sat, 16 Jul 2022 14:28:23 +0200 Subject: [PATCH 49/52] Somehow better than before but with Marlin... Sometimes it get stuck though --- server/hardware/device/comunication/device_serial.py | 1 + server/hardware/device/firmwares/commands_buffer.py | 2 +- server/hardware/device/firmwares/generic_firmware.py | 2 +- server/hardware/device/firmwares/marlin.py | 7 ++++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/server/hardware/device/comunication/device_serial.py b/server/hardware/device/comunication/device_serial.py index 86da38d2..6c351ebd 100644 --- a/server/hardware/device/comunication/device_serial.py +++ b/server/hardware/device/comunication/device_serial.py @@ -184,6 +184,7 @@ def _send_line(self): with self._mutex: line = self._send_buffer.pop(0) self.serial.write(str(line).encode()) + sleep(0.01) except: self.close() self.logger.error("Error while sending a command") diff --git a/server/hardware/device/firmwares/commands_buffer.py b/server/hardware/device/firmwares/commands_buffer.py index a0b916a2..06886c9f 100644 --- a/server/hardware/device/firmwares/commands_buffer.py +++ b/server/hardware/device/firmwares/commands_buffer.py @@ -96,7 +96,7 @@ def check_buffer_mutex_status(self): Check if the send lock must be released """ with self._mutex: - if self._send_mutex.locked() and len(self._buffer) < self._buffer_max_length: + if self._send_mutex.locked() and (len(self._buffer) < self._buffer_max_length): self._send_mutex.release() def popleft(self): diff --git a/server/hardware/device/firmwares/generic_firmware.py b/server/hardware/device/firmwares/generic_firmware.py index cd9974d8..5eb9e3a0 100644 --- a/server/hardware/device/firmwares/generic_firmware.py +++ b/server/hardware/device/firmwares/generic_firmware.py @@ -53,7 +53,7 @@ def __init__(self, serial_settings, logger, event_handler: FirwmareEventHandler) self.estimator = GenericEstimator() # buffer control - self._buffer = CommandBuffer(8) + self._buffer = CommandBuffer(2) # timeout setup self.force_ack_command = "" # command used to force an ack diff --git a/server/hardware/device/firmwares/marlin.py b/server/hardware/device/firmwares/marlin.py index efbabc3f..5bef66eb 100644 --- a/server/hardware/device/firmwares/marlin.py +++ b/server/hardware/device/firmwares/marlin.py @@ -140,6 +140,8 @@ def _resend(self, line_number): items = deepcopy(self.buffer._buffer_history) first_available_line = None + self.buffer.clear() + # resend the commands for command_n, command in items.items(): n_line_number = int(command_n.strip("N")) @@ -150,15 +152,14 @@ def _resend(self, line_number): first_available_line = line_number # All the lines after the required one must be resent. Cannot break the loop now self._serial_device.send(command) + self.buffer.push_command(command, n_line_number) # clear the serial queue from other "resend callbacks" self._serial_device.filter_callbacks_queue( lambda x: not ("Resend:" in x or "Error:Line Number" in x) ) - if line_found: - self.buffer.ack_received(safe_line_number=line_number - 1) - else: + if not line_found: self._logger.error("No line was found for the number required. Restart numeration.") # will reset the buffer and restart the numeration self.reset_status() From b7f28d92409f7f0290a48435ca271fcd03906bf7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:14:45 +0100 Subject: [PATCH 50/52] Bump terser from 4.8.0 to 4.8.1 in /frontend (#80) Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: Luca Tessaro Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index efe857b3..3c5ac0d5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -10817,9 +10817,9 @@ terser-webpack-plugin@^1.4.3: worker-farm "^1.7.0" terser@^4.1.2, terser@^4.6.2, terser@^4.6.3: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1" From 901440c0d6362e7556af59cfe4fea4065b0e5ca3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:15:24 +0100 Subject: [PATCH 51/52] Bump mako from 1.1.6 to 1.2.2 (#81) Bumps [mako](https://github.com/sqlalchemy/mako) from 1.1.6 to 1.2.2. - [Release notes](https://github.com/sqlalchemy/mako/releases) - [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/mako/commits) --- updated-dependencies: - dependency-name: mako dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: Luca Tessaro Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 448285b0..a134d662 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ isort==5.10.1 itsdangerous==2.0.1 Jinja2==3.0.3 lazy-object-proxy==1.7.1 -Mako==1.1.6 +Mako==1.2.2 MarkupSafe==2.0.1 mccabe==0.6.1 mypy-extensions==0.4.3 From bf4faf58c66d050bd60c548d16a7b7ad5f95a53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:16:09 +0100 Subject: [PATCH 52/52] Bump socket.io-parser from 3.3.2 to 3.3.3 in /frontend (#82) Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 3.3.2 to 3.3.3. - [Release notes](https://github.com/socketio/socket.io-parser/releases) - [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io-parser/compare/3.3.2...3.3.3) --- updated-dependencies: - dependency-name: socket.io-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: Luca Tessaro Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3c5ac0d5..07850338 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6315,7 +6315,7 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: isarray@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" - integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + integrity sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ== isexe@^2.0.0: version "2.0.0" @@ -7508,7 +7508,7 @@ move-concurrently@^1.0.1: ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.2: version "2.1.2" @@ -10257,9 +10257,9 @@ socket.io-client@^2.3.1: to-array "0.1.4" socket.io-parser@~3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.2.tgz#ef872009d0adcf704f2fbe830191a14752ad50b6" - integrity sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg== + version "3.3.3" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.3.tgz#3a8b84823eba87f3f7624e64a8aaab6d6318a72f" + integrity sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg== dependencies: component-emitter "~1.3.0" debug "~3.1.0"