Loading README.md +2 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,8 @@ Fablab 3d printing request application # Target Architecture Right now DFPM is not to be implemented ``` +-------------------------------------------------------------------------------------------------------------------------+ | |---------------------------------+ | Loading back/docker-compose.yml +15 −2 Original line number Diff line number Diff line version: '2' services: app: image: dvic.devinci.fr/server/myfab:latest ports: - 7414:80 environment: - MYSQL_HOST=db - MYSQL_DB=db - MYSQL_USER=user - MYSQL_PASSWORD=password links: - db db: image: "mysql" environment: Loading @@ -8,5 +19,7 @@ services: MYSQL_DATABASE: db MYSQL_USER: user MYSQL_PASSWORD: password ports: - "3306:3306" No newline at end of file # rtsp_streamer: # image: aler9/rtsp-simple-server # network: host back/myfab/api.py +165 −25 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ from myfab.model import ( init_db_connection, Queue, QueueElement, PrinterQueue, Message, ) from flask_api import status from flask import jsonify Loading @@ -23,6 +25,14 @@ import string import random import os from flask_cors import CORS from myfab.queue import ( get_printers_from_db, get_printers, get_printer_by_id, get_printers_for_queue, get_queue_by_id, delete_printer ) init_db_connection() log.info("Database connected") Loading Loading @@ -162,7 +172,6 @@ def loop_requests(reqs): def get_access_level_to_request(req: PrintRequest, user): # req is id or req same for user # TODO if req is not PrintRequest: req = get_request_by_id(req) if req is None: Loading Loading @@ -372,21 +381,71 @@ def update_user(username): # region Message system routes @app.route("/message/<request_id>") @jwt_required def get_messages(request_id): pass messages = [] req = get_request_by_id(request_id) if req == None: return jsonify({"error": "No such request"}), status.HTTP_404_NOT_FOUND if get_access_level_to_request(req, current_user) < REQUEST_ACCESS_OBSERVER: return jsonify({"error": "Access Denied"}), status.HTTP_401_UNAUTHORIZED for msg in ( Message.select() .where(Message.request == req) .order_by(Message.id) .desc() .limit(30) ): messages.append( { "req": msg.request.id, "author": msg.author.fullname, "text": msg, "creation": msg.creation, "id": msg.id, "read": msg.read, } ) if msg.author != current_user: msg.read = True msg.save() return jsonify(messages) @app.route("/message/<request_id>", methods=["POST"]) @jwt_required def post_message(request_id): pass data = request.get_json() req = get_request_by_id(request_id) if not req: return jsonify({"error": "No such request"}), status.HTTP_404_NOT_FOUND Message.create(request=req, text=data["message"], author=current_user) return jsonify({}) # @app.route("/message/unread") # @jwt_required # def unread_messages(): # messages = [] # for msg in Message.select().where( # Message.request.author == current_user and Message.read == False # ): # messages.append( # { # "req": msg.request.id, # "author": msg.author.fullname, # "text": msg, # "creation": msg.creation, # "id": msg.id, # } # ) # endregion # region printer management routes TODO # region printer management routes # list printers & status @app.route("/printers") Loading @@ -407,44 +466,98 @@ def list_printers(): } ] """ pass printers = [] for printer in get_printers(): printers.append({ "id": printer.id, "name": printer.name, "current_print_request": printer.current_print.id, "version": printer.version(), "printer_type": printer.printer_type(), "status": printer.status(), "printer_progress": printer.print_progress(), "infos": printer.infos(), "remaining_print_time": printer.remaining_print_time() }) return jsonify(printers) # action on printer TODO v2 @app.route("/printers/<printer_id>/action", methods=["POST"]) @app.route("/printers/<printer_id>/action/<action>", methods=["GET"]) @jwt_required def action_printer(printer_id): request.get_json() def action_printer(printer_id, action): """ { "action": "pause", "resume", "cancel", "cycle", "disable", "enable", "restart" } pause resume cacel are self explainatory cycle: goes to the next file in queue, implies no print job is running. Display a warn message first disable: put printer out of commission enable: !disable """ pass # Edit printer config # Edit printer config exceot queues @app.route("/printers/<printer_id>", methods=["PUT"]) @jwt_required def update_printer(printer_id): pass printer = get_printer_by_id(printer_id) if not printer: return jsonify({'error': 'No such printer'}), status.HTTP_404_NOT_FOUND printer_update = request.get_json() for elem in printer_update: setattr(printer.printer_obj, elem, printer_update[elem]) printer.printer_obj.save() return jsonify({}) @app.route("/printers/<printer_id>/queue/<qid>", methods=["PUT", "DELETE"]) @jwt_required def printer_queue(printer_id, qid): printer = get_printer_by_id(printer_id) if not printer: return jsonify({'error': 'No such priner'}), status.HTTP_404_NOT_FOUND q = get_queue_by_id(qid) if not q: return jsonify({'error': 'No such queue'}), status.HTTP_404_NOT_FOUND if request.method == "PUT": for q in printer.queues: if q.id == qid: return jsonify({'error': 'Printer already attached to this queue'}), status.HTTP_409_CONFLICT PrinterQueue.create(printer=printer, queue=q) return jsonify({}) else: for q in printer.queues: if q.id == qid: PrinterQueue.delete().where(PrinterQueue.printer=printer and PrinterQueue.queue=q) return jsonify({}) return jsonify({'error': 'Printer is not attched to this queue'}), status.HTTP_404_NOT_FOUND # Add printer config @app.route("/printers/new", methods=["POST"]) @app.route("/printers", methods=["POST"]) @jwt_required def add_new_printer(): pass printer_info = request.get_json() Printer.create(name=printer_info['name'], model=printer_info['model'], config=printer_info['config']) return jsonify({}) @app.route("/printers/<printer_id>", methods=["DELETE"]) @jwt_required def remove_printer(printer_id): #TODO check access level delete_printer(printer_id) return jsonify({}) # endregion printer management # region queues management TODO # region queues management routes # list queues @app.route("/queues") @app.route("/queues", methods=["GET", "POST"]) @jwt_required def list_queues(): def manage_queues(): """ [ { Loading @@ -452,19 +565,46 @@ def list_queues(): "name": "queue_name", "weight": 0, queue weight, "printers": [ids ],* "enabled": true, "process_span": when to process the queue (type unknown) ?? FIXME "enabled": true } ] """ pass # create queue # delete queue if request.method == "GET": # get queues qs = [] for q in get_queues(): qs.append({ "id": q.id, "name": q.name, "weight": q.weight, "printers": map(getprinters_for_queue(q), lambda p: {'id': p.id, 'name': p.name}) "enabled": true }) return jsonify({'printers': qs}) elif request.method == "POST": # create new queue queue_info = request.get_json() Queue.create(enabled = True, name=queue_info['name'], weight=queue_info['weight'], meta=queue_info['meta']) return jsonify({}) else: return jsonify({'error': 'Method not allowed'}), status.HTTP_405_METHOD_NOT_ALLOWED # update queue @app.route("/queues/<qid>", methods=["DELETE", "PUT"]) @jwt_required def manage_queues_with_id(qid): if request.method == "DELETE": # :) remove_queue(qid) return jsonify({}) elif request.method == "PUT": # update queue info Queue.update(**request.get_json()).where(Queue.id==quid) return jsonify({}) else: return jsonify({'error': 'Method not allowed'}), status.HTTP_405_METHOD_NOT_ALLOWED # endregion queues Loading back/myfab/model.py +55 −30 Original line number Diff line number Diff line from peewee import Model, MySQLDatabase, AutoField, IntegerField, CharField, TextField, ForeignKeyField, DateTimeField, DoubleField, BooleanField from peewee import ( Model, MySQLDatabase, AutoField, IntegerField, CharField, TextField, ForeignKeyField, DateTimeField, DoubleField, BooleanField, ) import datetime import sys, os import myfab.log as log CONNECTION = None def create_mysql_connection() -> MySQLDatabase: global CONNECTION assert CONNECTION == None if not 'MYSQL_HOST' in os.environ: os.environ['MYSQL_HOST'] = "127.0.0.1" os.environ['MYSQL_DB'] = 'db' os.environ['MYSQL_USER'] = 'user' os.environ['MYSQL_PASSWORD'] = 'password' log.warn('No MYSQL_HOST, using default values') url = os.environ['MYSQL_HOST'] if not "MYSQL_HOST" in os.environ: os.environ["MYSQL_HOST"] = "127.0.0.1" os.environ["MYSQL_DB"] = "db" os.environ["MYSQL_USER"] = "user" os.environ["MYSQL_PASSWORD"] = "password" log.warn("No MYSQL_HOST, using default values") url = os.environ["MYSQL_HOST"] log.info("Using MYSQL_HOST %s" % url) return MySQLDatabase(os.environ['MYSQL_DB'], user=os.environ['MYSQL_USER'], password=os.environ['MYSQL_PASSWORD'], port=3306) return MySQLDatabase( os.environ["MYSQL_DB"], user=os.environ["MYSQL_USER"], host = url, password=os.environ["MYSQL_PASSWORD"], port=3306, ) def init_db_connection(): Loading @@ -37,11 +52,14 @@ class User(Model): fullname = CharField() access = IntegerField(default=0) class Queue(Model): id = AutoField(primary_key=True) enabled = BooleanField() name = CharField() weight = IntegerField(default=1) meta = TextField() # json class QueueElement(Model): id = AutoField() Loading @@ -49,12 +67,15 @@ class QueueElement(Model): queue = ForeignKeyField(Queue) time_added = DateTimeField(default=datetime.datetime.now()) class PrintRequest(Model): id = AutoField(primary_key=True) title = CharField() description = TextField() creation = DateTimeField(default=datetime.datetime.now(), column_name='creation') last_modification = DateTimeField(default=datetime.datetime.now(), column_name='last_modification') creation = DateTimeField(default=datetime.datetime.now(), column_name="creation") last_modification = DateTimeField( default=datetime.datetime.now(), column_name="last_modification" ) author = ForeignKeyField(User) reject_message = CharField() stl = TextField(null=True, default=None) Loading @@ -62,15 +83,20 @@ class PrintRequest(Model): status = IntegerField(default=0) project = TextField() operator = ForeignKeyField(User, null=True, default=None) queue_element = ForeignKeyField(QueueElement, null=True, default=None) # Only the queue info, the actual queue is not store in db queue_element = ForeignKeyField( QueueElement, null=True, default=None ) # Only the queue info, the actual queue is not store in db recup_id = CharField(default="") class Message(Model): id = AutoField(primary_key=True) author = ForeignKeyField(User, null=False) text = TextField() creation = DateTimeField(default=datetime.datetime.now(), column_name='creation') creation = DateTimeField(default=datetime.datetime.now(), column_name="creation") request = ForeignKeyField(PrintRequest) read = BooleanField(default=False) class Event(Model): # to form timeline request = ForeignKeyField(PrintRequest) Loading @@ -78,6 +104,7 @@ class Event(Model): # to form timeline date = DateTimeField(default=datetime.datetime.now()) operartor = ForeignKeyField(User, null=True, default=None) class Printer(Model): # with connection information id = AutoField() name = CharField() Loading @@ -85,14 +112,12 @@ class Printer(Model): # with connection information config = CharField() # json current_print = ForeignKeyField(PrintRequest, null=True) class PrinterQueue(Model): printer = ForeignKeyField(Printer) queue = ForeignKeyField(Queue) # V2, Slicer vm info ? maybe not even necessary # class Slicer: # pass back/myfab/queue.py +321 −146 Original line number Diff line number Diff line from myfab.log import info, warn, error from myfab.model import PrintRequest, QueueElement, Queue, Printer, PrinterQueue from myfab.model import PrinterQueue, PrintRequest, QueueElement, Queue, Printer, PrinterQueue import requests as r import json import myfab.log as log Loading @@ -9,8 +9,35 @@ QUEUE_ELEMENT_STATE_INQUEUE = 0 QUEUE_ELEMENT_STATE_DONE = 1 QUEUE_ELEMENT_STATE_REMOVED = 2 PRINTERS = None # region QUEUE UTILS """ Get queues in the DB """ def get_queues(): return Queue.select().get() def get_printers_for_queue(q: Queue): printers = [] for pq in PrinterQueue.select().where(PrinterQueue.queue == q): printers.append(pq.printer) return printers """ Remove queue TODO: what append to the items in the queue ? """ def remove_queue(qid): if not get_queue_by_id(qid): return Queue.delete().where(Queue.id == qid) def create_queue(name, weight, meta): Queue.create(name=name, weight=weight, meta=meta) """ Add request to the end of the queue """ Loading @@ -19,59 +46,141 @@ def enqueue(req : PrintRequest, queue): req.queue_element = elem.id return elem """ Peek the immediate next queue item """ def get_next_request(queue): try: return QueueElement.select().where(QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE and QueueElement.queue == queue).order_by(QueueElement.id).limit(1).get() return ( QueueElement.select() .where( QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE and QueueElement.queue == queue ) .order_by(QueueElement.id) .limit(1) .get() ) except peewee.DoesNotExist: return None def rm_queue_top(queue, reason=QUEUE_ELEMENT_STATE_REMOVED): e: QueueElement = get_next_request(queue) if e != None: e.state = reason e.save() """ Number of elements before queue_element. 0 means queue_element is the current top of the queue and next dequeued object """ def get_queue_element_pos(queue_element: QueueElement): return QueueElement.select().where(QueueElement.queue == queue_element.queue and QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE, QueueElement.id < queue_element.id).count() return ( QueueElement.select() .where( QueueElement.queue == queue_element.queue and QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE, QueueElement.id < queue_element.id, ) .count() ) def get_queue_by_id(qid): r = Queue.select().where(Queue.id == qid) if not r.count(): return None return r.get() # endregion # region PRINTER UTILS & class def get_printers_from_db(): printers = [] for p in Printer.select(): printers.append(build_printer(p)) return printers """ Get printer """ def build_printer(p: Printer): if p.model == "fake": # fake printer printers.append(FakePrinter(p)) return FakePrinter(p) elif p.model == "octoprint": # octo printer printers.append(OctoPrinter(p)) return OctoPrinter(p) else: raise Exception("Unknown printer model") class PrinterBase: def get_printer_by_id(printer_id): for p in get_printers(): if p.id == printer_id: return p return None def pull_printers(): # Called once to fetch printers from db global PRINTERS PRINTERS = get_printers_from_db() def get_printers(): global PRINTERS if not PRINTERS: pull_printers() return PRINTERS def delete_printer(printer_id): global PRINTERS PRINTERS = list(filter(lambda printer: printer.id() != printer_id, PRINTERS)) class PrinterBase: def __init__(self, printer_obj): self.printer_obj = printer_obj self.id = printer_obj.id self.name = printer_obj.name self.current_print : PrintRequest = None self.printer_obj: Printer = printer_obj # self.id = printer_obj.id # self.name = printer_obj.name self.queues = [] self.load_queues() @property def id(self): return self.printer_obj.id @property def name(self): return self.printer_obj.name @property def current_print(self): return self.printer_obj.current_print def load_queues(self): self.queues = [] for q in PrinterQueue.select().where(PrinterQueue.printer == Printer.id): self.queues.append(q) def remove_queue(self, qid): pass # TODO def add_queue(self, qid): pass # TODO def set_print(self, pr: PrintRequest): raise NotImplementedError() Loading @@ -82,32 +191,44 @@ class PrinterBase: raise NotImplementedError() def printer_model(self): raise NotImplementedError() return self.printer_obj.model def refresh(self): raise NotImplementedError() def status(self): raise NotImplementedError() # Printer not linked to Octoprint but can represent a printer class FakePrinter(PrinterBase): def __init__(self, printer_obj): super().__init__(printer_obj) def refresh(self): pass def status(self): return "printing" if self.current_print != None else "idle" def infos(self): return {} def print_progress(self): return -1 def remaining_print_time(self): return -1 def printer_type(self): return "fake" def printer_model(self): return "N/A" def version(self): return "N/A" # Printer with Octoprint backend class OctoPrinter(PrinterBase): def __init__(self, printer_obj): super().__init__(printer_obj) config = printer_obj.config Loading @@ -118,7 +239,7 @@ class OctoPrinter(PrinterBase): 'key': the api-key } """ self._version = 'Unknown' self._version = "Unknown" self.hostname = config.hostname self.port = config.port self.key = config.key Loading @@ -126,10 +247,7 @@ class OctoPrinter(PrinterBase): self.test_and_pull_printer_global_info() self.load_queues() self.printer_state = { 'temperatures': { 'bed': float('nan'), 'tip': float('nan') }, "temperatures": {"bed": float("nan"), "tip": float("nan")}, "job": None, "state": { "text": "Unknown", Loading @@ -142,24 +260,44 @@ class OctoPrinter(PrinterBase): "sdReady": False, "error": False, "ready": False, "closedOrError": False } } "closedOrError": False, }, }, } self.reset_current_print() def set_print(self, print_obj: PrintRequest): if(not self.ready_for_printing): raise Exception('Printer is not ready') def start_print(self, print_obj: PrintRequest): self._refresh_printer_status() self._refresh_ready_state() if not self.ready_state: raise Exception("Printer is not ready") with open(os.path.join(print_obj.gcode)) as fh: #! FIXME path gcode = fh.read() self.printer_obj.current_print = print_obj self.current_print = None self.printer_obj.save() self._refresh_ready_state() log.info(f'Printer #{self.id} ({self.name}): Starting print #{self.current_print.id} ({self.current_print.title})') return self._make_octoprint_raw_request("/api/files/sdcard", method="post", data=None, files={'file': gcode, 'select': 'true', 'print': 'true'}) """ Can accept new print (or move safely at all) """ @property def ready_state(self): return self.ready_for_printing def _refresh_ready_state(self): self.ready_for_printing = self.printer_state['state']['flags']['ready'] and self.current_print == None self.ready_for_printing = ( self.printer_state["state"]["flags"]["ready"] and self.printer_state["state"]["flags"]["operational"] and self.current_print == None ) """ Object was removed from printing bed, printer is ready to operate again Object was removed from printing bed, printer is ready to operate again (unless octoprint says otherwise) """ def reset_current_print(self): self.current_print = None Loading @@ -168,36 +306,72 @@ class OctoPrinter(PrinterBase): self._refresh_printer_status() self._refresh_ready_state() def _make_octoprint_raw_request(self, path, method = 'get', data = None): return json.loads(r.request(method, f'http://{self.hostname}:{self.port}{path}', headers = {'X-Api-Key': self.key}, payload = data).text) def _make_octoprint_raw_request(self, path, method="get", data=None, files=None): return json.loads( r.request( method, f"http://{self.hostname}:{self.port}{path}", headers={"X-Api-Key": self.key}, payload=data, files=None ).text ) """ Home 3 axis """ def goto_home(self): if not self.ready_state: raise Exception("Printer is not ready") self._make_octoprint_raw_request("/api/printer/printhead", method="post", data=json.dumps({ "axes": ["x", "y", "z"], "command": "home" })) def _refresh_printer_status(self): log.info(f'Printer #{self.id} ({self.name}) @ {self.hostname}: refreshing status') status = self._make_octoprint_raw_request('/api/printer') log.info( f'Printer #{self.id} ({self.name}) @ {self.hostname}: refreshing status' ) status = self._make_octoprint_raw_request("/api/printer") self.printer_state = { "temperatures": { "bed": status['temperature']['bed']['actual'], "tip": status['temperature']['tool0']['actual'] "bed": status["temperature"]["bed"]["actual"], "tip": status["temperature"]["tool0"]["actual"], }, "state": status["state"] "state": status["state"], } if(not status['state']['flags']['printing']): self.printer_state['job'] = None #TODO: Event feed if not status["state"]["flags"]["printing"]: self.printer_state["job"] = None return job = self._make_octoprint_raw_request('/api/job') self.printer_state['job'] = { "printTime": job['progress']['printTime'], "printTimeLeft": job['progress']['printTimeLeft'], "progress": job['progress']['completion'], "print_length": job['filament']['length'] job = self._make_octoprint_raw_request("/api/job") self.printer_state["job"] = { "printTime": job["progress"]["printTime"], "printTimeLeft": job["progress"]["printTimeLeft"], "progress": job["progress"]["completion"], "print_length": job["filament"]["length"], } def test_and_pull_printer_global_info(self): versions = self._make_octoprint_raw_request('/api/version') log.info(f'Printer #{self.id} ({self.name}) @ {self.hostname}: connected to octoprint') versions = self._make_octoprint_raw_request("/api/version") log.info( f"Printer #{self.id} ({self.name}) @ {self.hostname}: connected to octoprint" ) log.info(f'Version info: {versions["text"]} API v{versions["text"]})') self._version = versions['text'] self._version = versions["text"] def print_progress(self): if not self.printer_state["job"]: return -1 return self.printer_state["job"]["progress"] def remaining_print_time(self): if not self.printer_state["job"]: return -1 return self.printer_state["job"]["printTimeLeft"] def infos(self): return self.printer_state def version(self): return self._version Loading @@ -205,8 +379,9 @@ class OctoPrinter(PrinterBase): def printer_type(self): return "Octoprint" def printer_model(self): return "wallah jsp" # FIXME xptdr def status(self): return self.printer_state["state"]["text"] # endregion Loading Loading
README.md +2 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,8 @@ Fablab 3d printing request application # Target Architecture Right now DFPM is not to be implemented ``` +-------------------------------------------------------------------------------------------------------------------------+ | |---------------------------------+ | Loading
back/docker-compose.yml +15 −2 Original line number Diff line number Diff line version: '2' services: app: image: dvic.devinci.fr/server/myfab:latest ports: - 7414:80 environment: - MYSQL_HOST=db - MYSQL_DB=db - MYSQL_USER=user - MYSQL_PASSWORD=password links: - db db: image: "mysql" environment: Loading @@ -8,5 +19,7 @@ services: MYSQL_DATABASE: db MYSQL_USER: user MYSQL_PASSWORD: password ports: - "3306:3306" No newline at end of file # rtsp_streamer: # image: aler9/rtsp-simple-server # network: host
back/myfab/api.py +165 −25 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ from myfab.model import ( init_db_connection, Queue, QueueElement, PrinterQueue, Message, ) from flask_api import status from flask import jsonify Loading @@ -23,6 +25,14 @@ import string import random import os from flask_cors import CORS from myfab.queue import ( get_printers_from_db, get_printers, get_printer_by_id, get_printers_for_queue, get_queue_by_id, delete_printer ) init_db_connection() log.info("Database connected") Loading Loading @@ -162,7 +172,6 @@ def loop_requests(reqs): def get_access_level_to_request(req: PrintRequest, user): # req is id or req same for user # TODO if req is not PrintRequest: req = get_request_by_id(req) if req is None: Loading Loading @@ -372,21 +381,71 @@ def update_user(username): # region Message system routes @app.route("/message/<request_id>") @jwt_required def get_messages(request_id): pass messages = [] req = get_request_by_id(request_id) if req == None: return jsonify({"error": "No such request"}), status.HTTP_404_NOT_FOUND if get_access_level_to_request(req, current_user) < REQUEST_ACCESS_OBSERVER: return jsonify({"error": "Access Denied"}), status.HTTP_401_UNAUTHORIZED for msg in ( Message.select() .where(Message.request == req) .order_by(Message.id) .desc() .limit(30) ): messages.append( { "req": msg.request.id, "author": msg.author.fullname, "text": msg, "creation": msg.creation, "id": msg.id, "read": msg.read, } ) if msg.author != current_user: msg.read = True msg.save() return jsonify(messages) @app.route("/message/<request_id>", methods=["POST"]) @jwt_required def post_message(request_id): pass data = request.get_json() req = get_request_by_id(request_id) if not req: return jsonify({"error": "No such request"}), status.HTTP_404_NOT_FOUND Message.create(request=req, text=data["message"], author=current_user) return jsonify({}) # @app.route("/message/unread") # @jwt_required # def unread_messages(): # messages = [] # for msg in Message.select().where( # Message.request.author == current_user and Message.read == False # ): # messages.append( # { # "req": msg.request.id, # "author": msg.author.fullname, # "text": msg, # "creation": msg.creation, # "id": msg.id, # } # ) # endregion # region printer management routes TODO # region printer management routes # list printers & status @app.route("/printers") Loading @@ -407,44 +466,98 @@ def list_printers(): } ] """ pass printers = [] for printer in get_printers(): printers.append({ "id": printer.id, "name": printer.name, "current_print_request": printer.current_print.id, "version": printer.version(), "printer_type": printer.printer_type(), "status": printer.status(), "printer_progress": printer.print_progress(), "infos": printer.infos(), "remaining_print_time": printer.remaining_print_time() }) return jsonify(printers) # action on printer TODO v2 @app.route("/printers/<printer_id>/action", methods=["POST"]) @app.route("/printers/<printer_id>/action/<action>", methods=["GET"]) @jwt_required def action_printer(printer_id): request.get_json() def action_printer(printer_id, action): """ { "action": "pause", "resume", "cancel", "cycle", "disable", "enable", "restart" } pause resume cacel are self explainatory cycle: goes to the next file in queue, implies no print job is running. Display a warn message first disable: put printer out of commission enable: !disable """ pass # Edit printer config # Edit printer config exceot queues @app.route("/printers/<printer_id>", methods=["PUT"]) @jwt_required def update_printer(printer_id): pass printer = get_printer_by_id(printer_id) if not printer: return jsonify({'error': 'No such printer'}), status.HTTP_404_NOT_FOUND printer_update = request.get_json() for elem in printer_update: setattr(printer.printer_obj, elem, printer_update[elem]) printer.printer_obj.save() return jsonify({}) @app.route("/printers/<printer_id>/queue/<qid>", methods=["PUT", "DELETE"]) @jwt_required def printer_queue(printer_id, qid): printer = get_printer_by_id(printer_id) if not printer: return jsonify({'error': 'No such priner'}), status.HTTP_404_NOT_FOUND q = get_queue_by_id(qid) if not q: return jsonify({'error': 'No such queue'}), status.HTTP_404_NOT_FOUND if request.method == "PUT": for q in printer.queues: if q.id == qid: return jsonify({'error': 'Printer already attached to this queue'}), status.HTTP_409_CONFLICT PrinterQueue.create(printer=printer, queue=q) return jsonify({}) else: for q in printer.queues: if q.id == qid: PrinterQueue.delete().where(PrinterQueue.printer=printer and PrinterQueue.queue=q) return jsonify({}) return jsonify({'error': 'Printer is not attched to this queue'}), status.HTTP_404_NOT_FOUND # Add printer config @app.route("/printers/new", methods=["POST"]) @app.route("/printers", methods=["POST"]) @jwt_required def add_new_printer(): pass printer_info = request.get_json() Printer.create(name=printer_info['name'], model=printer_info['model'], config=printer_info['config']) return jsonify({}) @app.route("/printers/<printer_id>", methods=["DELETE"]) @jwt_required def remove_printer(printer_id): #TODO check access level delete_printer(printer_id) return jsonify({}) # endregion printer management # region queues management TODO # region queues management routes # list queues @app.route("/queues") @app.route("/queues", methods=["GET", "POST"]) @jwt_required def list_queues(): def manage_queues(): """ [ { Loading @@ -452,19 +565,46 @@ def list_queues(): "name": "queue_name", "weight": 0, queue weight, "printers": [ids ],* "enabled": true, "process_span": when to process the queue (type unknown) ?? FIXME "enabled": true } ] """ pass # create queue # delete queue if request.method == "GET": # get queues qs = [] for q in get_queues(): qs.append({ "id": q.id, "name": q.name, "weight": q.weight, "printers": map(getprinters_for_queue(q), lambda p: {'id': p.id, 'name': p.name}) "enabled": true }) return jsonify({'printers': qs}) elif request.method == "POST": # create new queue queue_info = request.get_json() Queue.create(enabled = True, name=queue_info['name'], weight=queue_info['weight'], meta=queue_info['meta']) return jsonify({}) else: return jsonify({'error': 'Method not allowed'}), status.HTTP_405_METHOD_NOT_ALLOWED # update queue @app.route("/queues/<qid>", methods=["DELETE", "PUT"]) @jwt_required def manage_queues_with_id(qid): if request.method == "DELETE": # :) remove_queue(qid) return jsonify({}) elif request.method == "PUT": # update queue info Queue.update(**request.get_json()).where(Queue.id==quid) return jsonify({}) else: return jsonify({'error': 'Method not allowed'}), status.HTTP_405_METHOD_NOT_ALLOWED # endregion queues Loading
back/myfab/model.py +55 −30 Original line number Diff line number Diff line from peewee import Model, MySQLDatabase, AutoField, IntegerField, CharField, TextField, ForeignKeyField, DateTimeField, DoubleField, BooleanField from peewee import ( Model, MySQLDatabase, AutoField, IntegerField, CharField, TextField, ForeignKeyField, DateTimeField, DoubleField, BooleanField, ) import datetime import sys, os import myfab.log as log CONNECTION = None def create_mysql_connection() -> MySQLDatabase: global CONNECTION assert CONNECTION == None if not 'MYSQL_HOST' in os.environ: os.environ['MYSQL_HOST'] = "127.0.0.1" os.environ['MYSQL_DB'] = 'db' os.environ['MYSQL_USER'] = 'user' os.environ['MYSQL_PASSWORD'] = 'password' log.warn('No MYSQL_HOST, using default values') url = os.environ['MYSQL_HOST'] if not "MYSQL_HOST" in os.environ: os.environ["MYSQL_HOST"] = "127.0.0.1" os.environ["MYSQL_DB"] = "db" os.environ["MYSQL_USER"] = "user" os.environ["MYSQL_PASSWORD"] = "password" log.warn("No MYSQL_HOST, using default values") url = os.environ["MYSQL_HOST"] log.info("Using MYSQL_HOST %s" % url) return MySQLDatabase(os.environ['MYSQL_DB'], user=os.environ['MYSQL_USER'], password=os.environ['MYSQL_PASSWORD'], port=3306) return MySQLDatabase( os.environ["MYSQL_DB"], user=os.environ["MYSQL_USER"], host = url, password=os.environ["MYSQL_PASSWORD"], port=3306, ) def init_db_connection(): Loading @@ -37,11 +52,14 @@ class User(Model): fullname = CharField() access = IntegerField(default=0) class Queue(Model): id = AutoField(primary_key=True) enabled = BooleanField() name = CharField() weight = IntegerField(default=1) meta = TextField() # json class QueueElement(Model): id = AutoField() Loading @@ -49,12 +67,15 @@ class QueueElement(Model): queue = ForeignKeyField(Queue) time_added = DateTimeField(default=datetime.datetime.now()) class PrintRequest(Model): id = AutoField(primary_key=True) title = CharField() description = TextField() creation = DateTimeField(default=datetime.datetime.now(), column_name='creation') last_modification = DateTimeField(default=datetime.datetime.now(), column_name='last_modification') creation = DateTimeField(default=datetime.datetime.now(), column_name="creation") last_modification = DateTimeField( default=datetime.datetime.now(), column_name="last_modification" ) author = ForeignKeyField(User) reject_message = CharField() stl = TextField(null=True, default=None) Loading @@ -62,15 +83,20 @@ class PrintRequest(Model): status = IntegerField(default=0) project = TextField() operator = ForeignKeyField(User, null=True, default=None) queue_element = ForeignKeyField(QueueElement, null=True, default=None) # Only the queue info, the actual queue is not store in db queue_element = ForeignKeyField( QueueElement, null=True, default=None ) # Only the queue info, the actual queue is not store in db recup_id = CharField(default="") class Message(Model): id = AutoField(primary_key=True) author = ForeignKeyField(User, null=False) text = TextField() creation = DateTimeField(default=datetime.datetime.now(), column_name='creation') creation = DateTimeField(default=datetime.datetime.now(), column_name="creation") request = ForeignKeyField(PrintRequest) read = BooleanField(default=False) class Event(Model): # to form timeline request = ForeignKeyField(PrintRequest) Loading @@ -78,6 +104,7 @@ class Event(Model): # to form timeline date = DateTimeField(default=datetime.datetime.now()) operartor = ForeignKeyField(User, null=True, default=None) class Printer(Model): # with connection information id = AutoField() name = CharField() Loading @@ -85,14 +112,12 @@ class Printer(Model): # with connection information config = CharField() # json current_print = ForeignKeyField(PrintRequest, null=True) class PrinterQueue(Model): printer = ForeignKeyField(Printer) queue = ForeignKeyField(Queue) # V2, Slicer vm info ? maybe not even necessary # class Slicer: # pass
back/myfab/queue.py +321 −146 Original line number Diff line number Diff line from myfab.log import info, warn, error from myfab.model import PrintRequest, QueueElement, Queue, Printer, PrinterQueue from myfab.model import PrinterQueue, PrintRequest, QueueElement, Queue, Printer, PrinterQueue import requests as r import json import myfab.log as log Loading @@ -9,8 +9,35 @@ QUEUE_ELEMENT_STATE_INQUEUE = 0 QUEUE_ELEMENT_STATE_DONE = 1 QUEUE_ELEMENT_STATE_REMOVED = 2 PRINTERS = None # region QUEUE UTILS """ Get queues in the DB """ def get_queues(): return Queue.select().get() def get_printers_for_queue(q: Queue): printers = [] for pq in PrinterQueue.select().where(PrinterQueue.queue == q): printers.append(pq.printer) return printers """ Remove queue TODO: what append to the items in the queue ? """ def remove_queue(qid): if not get_queue_by_id(qid): return Queue.delete().where(Queue.id == qid) def create_queue(name, weight, meta): Queue.create(name=name, weight=weight, meta=meta) """ Add request to the end of the queue """ Loading @@ -19,59 +46,141 @@ def enqueue(req : PrintRequest, queue): req.queue_element = elem.id return elem """ Peek the immediate next queue item """ def get_next_request(queue): try: return QueueElement.select().where(QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE and QueueElement.queue == queue).order_by(QueueElement.id).limit(1).get() return ( QueueElement.select() .where( QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE and QueueElement.queue == queue ) .order_by(QueueElement.id) .limit(1) .get() ) except peewee.DoesNotExist: return None def rm_queue_top(queue, reason=QUEUE_ELEMENT_STATE_REMOVED): e: QueueElement = get_next_request(queue) if e != None: e.state = reason e.save() """ Number of elements before queue_element. 0 means queue_element is the current top of the queue and next dequeued object """ def get_queue_element_pos(queue_element: QueueElement): return QueueElement.select().where(QueueElement.queue == queue_element.queue and QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE, QueueElement.id < queue_element.id).count() return ( QueueElement.select() .where( QueueElement.queue == queue_element.queue and QueueElement.state == QUEUE_ELEMENT_STATE_INQUEUE, QueueElement.id < queue_element.id, ) .count() ) def get_queue_by_id(qid): r = Queue.select().where(Queue.id == qid) if not r.count(): return None return r.get() # endregion # region PRINTER UTILS & class def get_printers_from_db(): printers = [] for p in Printer.select(): printers.append(build_printer(p)) return printers """ Get printer """ def build_printer(p: Printer): if p.model == "fake": # fake printer printers.append(FakePrinter(p)) return FakePrinter(p) elif p.model == "octoprint": # octo printer printers.append(OctoPrinter(p)) return OctoPrinter(p) else: raise Exception("Unknown printer model") class PrinterBase: def get_printer_by_id(printer_id): for p in get_printers(): if p.id == printer_id: return p return None def pull_printers(): # Called once to fetch printers from db global PRINTERS PRINTERS = get_printers_from_db() def get_printers(): global PRINTERS if not PRINTERS: pull_printers() return PRINTERS def delete_printer(printer_id): global PRINTERS PRINTERS = list(filter(lambda printer: printer.id() != printer_id, PRINTERS)) class PrinterBase: def __init__(self, printer_obj): self.printer_obj = printer_obj self.id = printer_obj.id self.name = printer_obj.name self.current_print : PrintRequest = None self.printer_obj: Printer = printer_obj # self.id = printer_obj.id # self.name = printer_obj.name self.queues = [] self.load_queues() @property def id(self): return self.printer_obj.id @property def name(self): return self.printer_obj.name @property def current_print(self): return self.printer_obj.current_print def load_queues(self): self.queues = [] for q in PrinterQueue.select().where(PrinterQueue.printer == Printer.id): self.queues.append(q) def remove_queue(self, qid): pass # TODO def add_queue(self, qid): pass # TODO def set_print(self, pr: PrintRequest): raise NotImplementedError() Loading @@ -82,32 +191,44 @@ class PrinterBase: raise NotImplementedError() def printer_model(self): raise NotImplementedError() return self.printer_obj.model def refresh(self): raise NotImplementedError() def status(self): raise NotImplementedError() # Printer not linked to Octoprint but can represent a printer class FakePrinter(PrinterBase): def __init__(self, printer_obj): super().__init__(printer_obj) def refresh(self): pass def status(self): return "printing" if self.current_print != None else "idle" def infos(self): return {} def print_progress(self): return -1 def remaining_print_time(self): return -1 def printer_type(self): return "fake" def printer_model(self): return "N/A" def version(self): return "N/A" # Printer with Octoprint backend class OctoPrinter(PrinterBase): def __init__(self, printer_obj): super().__init__(printer_obj) config = printer_obj.config Loading @@ -118,7 +239,7 @@ class OctoPrinter(PrinterBase): 'key': the api-key } """ self._version = 'Unknown' self._version = "Unknown" self.hostname = config.hostname self.port = config.port self.key = config.key Loading @@ -126,10 +247,7 @@ class OctoPrinter(PrinterBase): self.test_and_pull_printer_global_info() self.load_queues() self.printer_state = { 'temperatures': { 'bed': float('nan'), 'tip': float('nan') }, "temperatures": {"bed": float("nan"), "tip": float("nan")}, "job": None, "state": { "text": "Unknown", Loading @@ -142,24 +260,44 @@ class OctoPrinter(PrinterBase): "sdReady": False, "error": False, "ready": False, "closedOrError": False } } "closedOrError": False, }, }, } self.reset_current_print() def set_print(self, print_obj: PrintRequest): if(not self.ready_for_printing): raise Exception('Printer is not ready') def start_print(self, print_obj: PrintRequest): self._refresh_printer_status() self._refresh_ready_state() if not self.ready_state: raise Exception("Printer is not ready") with open(os.path.join(print_obj.gcode)) as fh: #! FIXME path gcode = fh.read() self.printer_obj.current_print = print_obj self.current_print = None self.printer_obj.save() self._refresh_ready_state() log.info(f'Printer #{self.id} ({self.name}): Starting print #{self.current_print.id} ({self.current_print.title})') return self._make_octoprint_raw_request("/api/files/sdcard", method="post", data=None, files={'file': gcode, 'select': 'true', 'print': 'true'}) """ Can accept new print (or move safely at all) """ @property def ready_state(self): return self.ready_for_printing def _refresh_ready_state(self): self.ready_for_printing = self.printer_state['state']['flags']['ready'] and self.current_print == None self.ready_for_printing = ( self.printer_state["state"]["flags"]["ready"] and self.printer_state["state"]["flags"]["operational"] and self.current_print == None ) """ Object was removed from printing bed, printer is ready to operate again Object was removed from printing bed, printer is ready to operate again (unless octoprint says otherwise) """ def reset_current_print(self): self.current_print = None Loading @@ -168,36 +306,72 @@ class OctoPrinter(PrinterBase): self._refresh_printer_status() self._refresh_ready_state() def _make_octoprint_raw_request(self, path, method = 'get', data = None): return json.loads(r.request(method, f'http://{self.hostname}:{self.port}{path}', headers = {'X-Api-Key': self.key}, payload = data).text) def _make_octoprint_raw_request(self, path, method="get", data=None, files=None): return json.loads( r.request( method, f"http://{self.hostname}:{self.port}{path}", headers={"X-Api-Key": self.key}, payload=data, files=None ).text ) """ Home 3 axis """ def goto_home(self): if not self.ready_state: raise Exception("Printer is not ready") self._make_octoprint_raw_request("/api/printer/printhead", method="post", data=json.dumps({ "axes": ["x", "y", "z"], "command": "home" })) def _refresh_printer_status(self): log.info(f'Printer #{self.id} ({self.name}) @ {self.hostname}: refreshing status') status = self._make_octoprint_raw_request('/api/printer') log.info( f'Printer #{self.id} ({self.name}) @ {self.hostname}: refreshing status' ) status = self._make_octoprint_raw_request("/api/printer") self.printer_state = { "temperatures": { "bed": status['temperature']['bed']['actual'], "tip": status['temperature']['tool0']['actual'] "bed": status["temperature"]["bed"]["actual"], "tip": status["temperature"]["tool0"]["actual"], }, "state": status["state"] "state": status["state"], } if(not status['state']['flags']['printing']): self.printer_state['job'] = None #TODO: Event feed if not status["state"]["flags"]["printing"]: self.printer_state["job"] = None return job = self._make_octoprint_raw_request('/api/job') self.printer_state['job'] = { "printTime": job['progress']['printTime'], "printTimeLeft": job['progress']['printTimeLeft'], "progress": job['progress']['completion'], "print_length": job['filament']['length'] job = self._make_octoprint_raw_request("/api/job") self.printer_state["job"] = { "printTime": job["progress"]["printTime"], "printTimeLeft": job["progress"]["printTimeLeft"], "progress": job["progress"]["completion"], "print_length": job["filament"]["length"], } def test_and_pull_printer_global_info(self): versions = self._make_octoprint_raw_request('/api/version') log.info(f'Printer #{self.id} ({self.name}) @ {self.hostname}: connected to octoprint') versions = self._make_octoprint_raw_request("/api/version") log.info( f"Printer #{self.id} ({self.name}) @ {self.hostname}: connected to octoprint" ) log.info(f'Version info: {versions["text"]} API v{versions["text"]})') self._version = versions['text'] self._version = versions["text"] def print_progress(self): if not self.printer_state["job"]: return -1 return self.printer_state["job"]["progress"] def remaining_print_time(self): if not self.printer_state["job"]: return -1 return self.printer_state["job"]["printTimeLeft"] def infos(self): return self.printer_state def version(self): return self._version Loading @@ -205,8 +379,9 @@ class OctoPrinter(PrinterBase): def printer_type(self): return "Octoprint" def printer_model(self): return "wallah jsp" # FIXME xptdr def status(self): return self.printer_state["state"]["text"] # endregion Loading