Commit 566dd824 authored by Grégor JOUET's avatar Grégor JOUET 🔧
Browse files

octoprint connector

parent 78ce803f
Loading
Loading
Loading
Loading
+36 −9
Original line number Diff line number Diff line
@@ -4,29 +4,56 @@ Fablab 3d printing request application

## Minimum Viable Version

- [ ] Tests
- [ ] *Tests*
- [x] User Connection
    - [ ] User administration
        - [ ] Backend
        - [ ] List users,
        - [ ] Change access level
- [ ] Submit a print request
- [x] Submit a print request
    - [x] Upload a STL file
        - [x] Backend
        - [x] Front
    - [x] Create a new Print Request
        - [x] Backend
        - [x] Front
- [ ] Accept/Reject print request (front)
- [ ] Edit the print request
    - [ ] Manualy set the state
    - [ ] Delete
    - [ ] Asign the request 
- [x] Accept/Reject print request (front)
    - [x] See STL file in 3d 
    - [x] See print status
    - [ ] Change Print status (see enum in api)
- [ ] Slice 
    - [ ] Upload GCODE
    - [x] Change Print status (see enum in api)
- [x] Slice 
    - [x] Upload GCODE
    - [x] Download GCODE
    - [ ] **V2**: Open slicer window
- [ ] Message system
    - [ ] Post message on each request
    - [ ] New message indication
- [ ] **V2**: Send to printer
- [ ] Queue management
    - [ ] Working queues
    - [ ] Printer management
    - [ ] Post to octoprint
    - [ ] Indicate successfull / error in printing
    - [ ] **v2** Printer management
    - [ ] **v2** Post to octoprint

# Target Architecture 

```
                 +-------------------------------------------------------------------------------------------------------------------------+
                 |                                                                                    |---------------------------------+  |
                 | +------------------------------------------------+       +----------------+        +---------------+                 |  |
                 | |                                                |  +--->+ DFPM           |        |               |  Printer        |  |
+--------+       | | +--------------+      +-------------------+    |  |    | Printer manager<--------> Octoprint  <---->               |  |
|Client  |       | | |              |      |                   <-------+    +----------------+        |               |                 |  |
|        <-----------> Front        <------> Request Manager   |    |                                 +---------------+-----------------+  |
|        |       | | |              |      |                   |    |     +----------------------------------------+                       |
+--------+       | | +--------------+      +-----------^-------+    |     |                                        |                       |
                 | |                                   |            |     +-------------+                          |                       |
                 | |                                   |            |     |             |    Slicer VM             |                       |
                 | |                                   +------------------>  Slicer     |                          |                       |
                 | +------------------------------------------------+     |  manager    |                          |                       |
                 |                                                        +----------------------------------------+                       |
                 +-------------------------------------------------------------------------------------------------------------------------+
```
 No newline at end of file
+133 −8
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ from flask_jwt_extended import (
)

import datetime
from myfab.model import Event, PrintRequest, User, init_db_connection, Queue
from myfab.model import Event, PrintRequest, User, init_db_connection, Queue, QueueElement
from flask_api import status
from flask import jsonify
import string
@@ -50,6 +50,10 @@ REQUEST_ACCESS_OBSERVER = 1 # Allowed to see this request
REQUEST_ACCESS_USER     = 2 # Is the author of this request
REQUEST_ACCESS_OPERATOR = 3 # Is an operator of the system

USER_ACCESS_NORMAL = 0
USER_ACCESS_OBSERVER = 1
USER_ACCESS_OPERATOR = 2

# Elements that can be modified by a user (not op)
REQUEST_USER_ELEMENTS = set(["title","description","project"])

@@ -108,6 +112,8 @@ def loop_requests(reqs):
    # if type(reqs) != list:
    #     reqs = [reqs]
    for req in reqs:
        if get_access_level_to_request(req, current_user) == REQUEST_ACCESS_DENIED:
            continue
        rqs.append({
            "request_id": req.id,
            "title": req.title,
@@ -129,7 +135,22 @@ def loop_requests(reqs):
def get_access_level_to_request(req: PrintRequest, user):
    #req is id or req same for user
    # TODO
    return REQUEST_ACCESS_OPERATOR # FIXME
    if req is not PrintRequest:
        req = get_request_by_id(req)
        if req is None:
            raise Exception("No such request")
    if user is not User:
        user = get_or_create_user(user)
    
    if user.access == USER_ACCESS_OPERATOR:
        return REQUEST_ACCESS_OPERATOR  # user is admin
    if user.access == USER_ACCESS_OBSERVER:
        return REQUEST_ACCESS_OBSERVER  # user has unlimited view
    if user.access == USER_ACCESS_NORMAL:
        if req.author == user.username: 
            return REQUEST_ACCESS_USER  # user is creator of the request
        else:
            return REQUEST_ACCESS_DENIED # user does not have access
    
def update_requires_operator_rights(update_json):
    s = set(update_json)
@@ -168,9 +189,8 @@ def update_request(req_id):

    required_access = REQUEST_ACCESS_OPERATOR if req.status > 0 or update_requires_operator_rights(changes) else REQUEST_ACCESS_USER

    if get_access_level_to_request(req, current_user) < REQUEST_ACCESS_OPERATOR:
    if get_access_level_to_request(req, current_user) < required_access:
        return jsonify({'error': 'Access denied'}), status.HTTP_401_UNAUTHORIZED
    # TODO access

    for e in changes:
        setattr(req, e, changes[e])
@@ -252,17 +272,119 @@ def get_requests(): #! FIXME Don't get request if not access

#region user management routes

# user list
@app.route("/users")
@jwt_required
def list_users():
    if current_user.access != USER_ACCESS_OPERATOR:
        return jsonify({'error': 'Access denied'}), status.HTTP_401_UNAUTHORIZED
    users = []
    for user in User.select():
        users.append({
            'username': user.username,
            'fullname': user.fullname,
            'access': user.access
        })

    return jsonify(users)

#change user TODO
@app.route("/users/<username>", methods=['PUT'])
@jwt_required
def update_user(username):
    if current_user.access != USER_ACCESS_OPERATOR:
        return jsonify({'error': 'Access Denied'}), status.HTTP_401_UNAUTHORIZED
    user = get_or_create_user(username) #FIXME no such user is 500
    changes = request.get_json()

    for e in changes:
        setattr(user, e, changes[e]) 

    user.save()
    return jsonify({})
   
#endregion user management

#region Printer manager routes
#region printer management routes TODO

# list printers & status
@app.route("/printers")
@jwt_required
def list_printers():
    """
    [
        {
            "id": "generated_id",
            "name": "given name by operators",
            "current_print_request": 0,
            "version": "octoprint & dfpm",
            "printer_type": "mini_prusa_mk3",
            "status": "printing, idle, error, out of commission, virtual printer...",
            "print_progress": 0.0,
            "infos": {temperatures etc...}
            "remaining_print_time"
        }
    ]
    """
    pass

# action on printer TODO v2
@app.route("/printers/<printer_id>/action", methods=["POST"])
@jwt_required
def action_printer(printer_id):
    request.get_json()
    """
    {
        "action": "pause", "resume", "cancel", "cycle", "disable", "enable", "restart"
    }
    """
    pass

# Edit printer config
@app.route("/printers/<printer_id>", methods=["PUT"])
@jwt_required
def update_printer(printer_id):
    pass

# Add printer config
@app.route("/printers/new", methods=["POST"])
@jwt_required
def add_new_printer():
    pass


#endregion printer management

#region queues management
#region queues management TODO

#list queues
@app.route("/queues")
@jwt_required
def list_queues():
    """
    [
        {
            "id": "id",
            "name": "queue_name",
            "weight": 0, queue weight,
            "printers": [ids ],*
            "enabled": true,
            "process_span": when to process the queue (type unknown) ?? FIXME
        }
    ]
    """
    pass

# create queue

# delete queue

# update queue


#endregion queues

#region Slicer manager routes
#region slicer manager routes TODO

"""
GET /slicer/update
@@ -292,6 +414,9 @@ POST /slicer/usage

#endregion routes




# peewee usage

# user = User.select().where(User.username == "test").count()
+17 −10
Original line number Diff line number Diff line
@@ -71,16 +71,23 @@ 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()
#     host = CharField()
#     username = CharField()
#     password = CharField()

# class PrinterQueue(Model):
#     printer = ForeignKeyField(Printer)
#     queue = ForeignKeyField(Queue)
class Printer(Model): # with connection information
    id = AutoField()
    name = CharField()
    model = CharField() 
    config = CharField() # json
    current_print = ForeignKeyField(PrintRequest)

class PrinterQueue(Model):
    printer = ForeignKeyField(Printer)
    queue = ForeignKeyField(Queue)

class QueueElement(Model):
    id = AutoField()
    state = IntegerField(default=0) # 0: todo, 1: done, 2: err/ canceled ?
    queue = ForeignKeyField(Queue)
    time_added = DateTimeField(default=datetime.datetime.now())



# V2, Slicer vm info ? maybe not even necessary
+183 −23
Original line number Diff line number Diff line
from myfab.log import info, warn, error
from myfab.model import PrintRequest
from myfab.model import PrintRequest, QueueElement, Queue, Printer, PrinterQueue
import requests as r
import json
import myfab.log as log

QUEUE_ELEMENT_STATE_INQUEUE = 0
QUEUE_ELEMENT_STATE_DONE = 1
QUEUE_ELEMENT_STATE_REMOVED = 2

#region QUEUE UTILS

"""
Queue manger for printers
  Add request to the end of the queue
"""
def enqueue(req, queue):
  pass

"""
  Get the immediate next item 
"""
def get_next_request(queue):
  pass

class Queue:
def rm_queue_top(queue):
  pass

def remove_queue_element(queue_element, reason):
  pass

"""
  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()

    def __init__(self, name):
        self.name = name
        self.printers = {}
        self._queue = []
#endregion

    def enqueue(self, request: PrintRequest):
        info(f'[QUEUE] Enqueue {request.id} ({request.title}) in {self.name}')
        self._queue.append(request)
#region PRINTER UTILS & class

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.queues = []

  def load_queues(self):
    self.queues = []
    for q in PrinterQueue.select().where(PrinterQueue.printer == Printer.id):
      self.queues.append(q)

  def set_print(self, pr : PrintRequest):
    raise NotImplementedError()    

  def printer_type(self):
    raise NotImplementedError()

  def version(self):
    raise NotImplementedError()

  def printer_model(self):
    raise NotImplementedError()

  def refresh(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 __call__(self):
        return self._queue[0]
  def printer_type(self):
    return "fake"

    def add_printer(self, printer, weight):
        self.printers[printer] = weight
  def printer_model(self):
    return "N/A"

    def pop(self):
        self._queue.pop()
        info(f'[QUEUE] Pop {self.name}')
  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
    """
An Octoprint compatible printer
     {
       'hostname': 'hostname or ip',
       'port': port
       'key': the api-key
     }
    """
class Printer:
    pass
    self._version = 'Unknown'
    self.hostname = config.hostname
    self.port = config.port
    self.key = config.key
    self.ready_for_printing = False # can accept new print
    self.test_and_pull_printer_global_info()
    self.load_queues()
    self.printer_state = {
      'temperatures': {
        'bed': float('nan'),
        'tip': float('nan')
      },
      "job": None,
      "state": {
        "text": "Unknown",
        "flags": {
          "operational": False,
          "paused": False,
          "printing": False,
          "cancelling": False,
          "pausing": False,
          "sdReady": False,
          "error": False,
          "ready": 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')
    self.printer_obj.current_print = print_obj
    self.current_print = None
    self.printer_obj.save()

  def _refresh_ready_state(self):
    self.ready_for_printing = self.printer_state['state']['flags']['ready'] and self.current_print == None
    
  """
    Object was removed from printing bed, printer is ready to operate again
  """
  def reset_current_print(self):
    self.current_print = None
    self.printer_obj.current_print = None
    self.printer_obj.save()
    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 _refresh_printer_status(self):
    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']
      },
      "state": status["state"]
    }
    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']
    }

  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')
    log.info(f'Version info: {versions["text"]} API v{versions["text"]})')
    self._version = versions['text']

  def version(self):
    return self._version

  def printer_type(self):
    return "Octoprint"

  def printer_model(self):
    return "wallah jsp" # FIXME xptdr

#endregion


# Octoprint api example

"""

Loading