WIP First work towards printer profiles
This commit is contained in:
parent
e55f11c5b9
commit
1957d2bb58
18 changed files with 568 additions and 46 deletions
|
|
@ -221,7 +221,7 @@ class SlicerPlugin(Plugin):
|
|||
def save_slicer_profile(self, path, profile, allow_overwrite=True, overrides=None):
|
||||
pass
|
||||
|
||||
def do_slice(self, model_path, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
pass
|
||||
|
||||
def cancel_slicing(self, machinecode_path):
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
|
||||
self._save_profile(path, new_profile, allow_overwrite=allow_overwrite)
|
||||
|
||||
def do_slice(self, model_path, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
def do_slice(self, model_path, printer_profile, machinecode_path=None, profile_path=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
if not profile_path:
|
||||
profile_path = s.get(["default_profile"])
|
||||
if not machinecode_path:
|
||||
|
|
@ -244,7 +244,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
|
||||
self._cura_logger.info("### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path))
|
||||
|
||||
engine_settings = self._convert_to_engine(profile_path)
|
||||
engine_settings = self._convert_to_engine(profile_path, printer_profile)
|
||||
|
||||
executable = s.get(["cura_engine"])
|
||||
if not executable:
|
||||
|
|
@ -385,8 +385,8 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
with open(path, "wb") as f:
|
||||
yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True)
|
||||
|
||||
def _convert_to_engine(self, profile_path):
|
||||
profile = Profile(self._load_profile(profile_path))
|
||||
def _convert_to_engine(self, profile_path, printer_profile):
|
||||
profile = Profile(self._load_profile(profile_path), printer_profile)
|
||||
return profile.convert_to_engine()
|
||||
|
||||
def _sanitize_name(name):
|
||||
|
|
|
|||
|
|
@ -515,8 +515,9 @@ class Profile(object):
|
|||
|
||||
return result
|
||||
|
||||
def __init__(self, profile, overrides=None):
|
||||
def __init__(self, profile, printer_profile, overrides=None):
|
||||
self._profile = self.__class__.merge_profile(profile, overrides=overrides)
|
||||
self._printer_profile = printer_profile
|
||||
|
||||
def profile(self):
|
||||
import copy
|
||||
|
|
@ -524,23 +525,22 @@ class Profile(object):
|
|||
|
||||
def get(self, key):
|
||||
if key in ("machine_width", "machine_depth", "machine_center_is_zero"):
|
||||
bedDimensions = s.globalGet(["printerParameters", "bedDimensions"])
|
||||
circular = bedDimensions["circular"] if "circular" in bedDimensions else False
|
||||
radius = bedDimensions["radius"] if "radius" in bedDimensions and bedDimensions["radius"] is not None else 0
|
||||
if key == "machine_width":
|
||||
return radius * 2 if circular else bedDimensions["x"]
|
||||
return self._printer_profile["volume"]["width"]
|
||||
elif key == "machine_depth":
|
||||
return radius * 2 if circular else bedDimensions["y"]
|
||||
return self._printer_profile["volume"]["depth"]
|
||||
elif key == "machine_height":
|
||||
return self._printer_profile["volume"]["height"]
|
||||
elif key == "machine_center_is_zero":
|
||||
return circular
|
||||
return self._printer_profile["volume"]["formFactor"] == "circular"
|
||||
else:
|
||||
return None
|
||||
|
||||
elif key == "extruder_amount":
|
||||
return s.globalGetInt(["printerParameters", "numExtruders"])
|
||||
return self._printer_profile["extruder"]["count"]
|
||||
|
||||
elif key.startswith("extruder_offset_"):
|
||||
extruder_offsets = s.globalGet(["printerParameters", "extruderOffsets"])
|
||||
extruder_offsets = self._printer_profile["extruder"]["offsets"]
|
||||
match = Profile.regex_extruder_offset.match(key)
|
||||
if not match:
|
||||
return 0.0
|
||||
|
|
@ -553,9 +553,13 @@ class Profile(object):
|
|||
return 0.0
|
||||
if number >= len(extruder_offsets):
|
||||
return 0.0
|
||||
if not axis in extruder_offsets[number]:
|
||||
|
||||
if axis == "x":
|
||||
return extruder_offsets[number][0]
|
||||
elif axis == "y":
|
||||
return extruder_offsets[number][1]
|
||||
else:
|
||||
return 0.0
|
||||
return extruder_offsets[number][axis]
|
||||
|
||||
elif key.startswith("filament_diameter"):
|
||||
match = Profile.regex_filament_diameter.match(key)
|
||||
|
|
|
|||
|
|
@ -30,13 +30,14 @@ def getConnectionOptions():
|
|||
}
|
||||
|
||||
class Printer():
|
||||
def __init__(self, fileManager, analysisQueue):
|
||||
def __init__(self, fileManager, analysisQueue, printerProfileManager):
|
||||
from collections import deque
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
self._analysisQueue = analysisQueue
|
||||
self._fileManager = fileManager
|
||||
self._printerProfileManager = printerProfileManager
|
||||
|
||||
# state
|
||||
# TODO do we really need to hold the temperature here?
|
||||
|
|
@ -159,7 +160,7 @@ class Printer():
|
|||
|
||||
#~~ printer commands
|
||||
|
||||
def connect(self, port=None, baudrate=None):
|
||||
def connect(self, port=None, baudrate=None, profile=None):
|
||||
"""
|
||||
Connects to the printer. If port and/or baudrate is provided, uses these settings, otherwise autodetection
|
||||
will be attempted.
|
||||
|
|
@ -167,6 +168,7 @@ class Printer():
|
|||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = comm.MachineCom(port, baudrate, callbackObject=self)
|
||||
self._printerProfileManager.select(profile)
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
|
|
@ -175,6 +177,7 @@ class Printer():
|
|||
if self._comm is not None:
|
||||
self._comm.close()
|
||||
self._comm = None
|
||||
self._printerProfileManager.deselect()
|
||||
eventManager().fire(Events.DISCONNECTED)
|
||||
|
||||
def command(self, command):
|
||||
|
|
@ -652,7 +655,8 @@ class Printer():
|
|||
return "Closed", None, None
|
||||
|
||||
port, baudrate = self._comm.getConnection()
|
||||
return self._comm.getStateString(), port, baudrate
|
||||
printer_profile = self._printerProfileManager.get_current_or_default()
|
||||
return self._comm.getStateString(), port, baudrate, printer_profile
|
||||
|
||||
def isClosedOrError(self):
|
||||
return self._comm is None or self._comm.isClosedOrError()
|
||||
209
src/octoprint/printer/profile.py
Normal file
209
src/octoprint/printer/profile.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import os
|
||||
import copy
|
||||
import re
|
||||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.util import dict_merge
|
||||
|
||||
class SaveError(Exception):
|
||||
pass
|
||||
|
||||
class BedTypes(object):
|
||||
RECTANGULAR = "rectangular"
|
||||
CIRCULAR = "circular"
|
||||
|
||||
class PrinterProfileManager(object):
|
||||
|
||||
default = dict(
|
||||
id = "_default",
|
||||
name = "Default",
|
||||
color = "default",
|
||||
volume=dict(
|
||||
width = 200,
|
||||
depth = 200,
|
||||
height = 200,
|
||||
formFactor = BedTypes.RECTANGULAR,
|
||||
),
|
||||
heatedBed = False,
|
||||
extruder=dict(
|
||||
count = 1,
|
||||
offsets = [
|
||||
(0, 0)
|
||||
]
|
||||
),
|
||||
axes=dict(
|
||||
x = dict(speed=6000, inverted=False),
|
||||
y = dict(speed=6000, inverted=False),
|
||||
z = dict(speed=200, inverted=False),
|
||||
e = dict(speed=300, inverted=False)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self._current = None
|
||||
|
||||
self._folder = settings().getBaseFolder("printerProfiles")
|
||||
|
||||
def select(self, identifier):
|
||||
if identifier is None or not self.exists(identifier):
|
||||
self._current = self.get_default()
|
||||
return False
|
||||
else:
|
||||
self._current = self.get(identifier)
|
||||
return True
|
||||
|
||||
def deselect(self):
|
||||
self._current = None
|
||||
|
||||
def get_all(self):
|
||||
return self._load_all()
|
||||
|
||||
def get(self, identifier):
|
||||
if identifier == "_default":
|
||||
return self._load_default()
|
||||
elif self.exists(identifier):
|
||||
return self._load_from_path(self._get_profile_path(identifier))
|
||||
else:
|
||||
return None
|
||||
|
||||
def remove(self, identifier):
|
||||
if identifier == "_default":
|
||||
return False
|
||||
return self._remove_from_path(self._get_profile_path(identifier))
|
||||
|
||||
def save(self, profile, allow_overwrite=False):
|
||||
if "id" in profile:
|
||||
identifier = profile["id"]
|
||||
elif "name" in profile:
|
||||
identifier = profile["name"]
|
||||
else:
|
||||
raise ValueError("profile must contain either id or name")
|
||||
|
||||
identifier = self._sanitize(identifier)
|
||||
|
||||
if identifier == "_default":
|
||||
default_profile = dict_merge(self._load_default(), profile)
|
||||
settings().set(["printerProfiles", "defaultProfile"], default_profile, defaults=dict(printerProfiles=dict(defaultProfile=self.__class__.default)))
|
||||
|
||||
profile["id"] = identifier
|
||||
self._save_to_path(self._get_profile_path(identifier), profile, allow_overwrite=allow_overwrite)
|
||||
return self.get(identifier)
|
||||
|
||||
def get_default(self):
|
||||
default = settings().get(["printerProfiles", "default"])
|
||||
if default is not None and self.exists(default):
|
||||
profile = self.get(default)
|
||||
if profile is not None:
|
||||
return profile
|
||||
|
||||
return self._load_default()
|
||||
|
||||
def set_default(self, identifier):
|
||||
all_identifiers = self._load_all_identifiers().keys()
|
||||
if identifier is not None and not identifier in all_identifiers:
|
||||
return
|
||||
|
||||
settings().set(["printerProfile", "default"], identifier)
|
||||
settings().save()
|
||||
|
||||
def get_current_or_default(self):
|
||||
if self._current is not None:
|
||||
return self._current
|
||||
else:
|
||||
return self.get_default()
|
||||
|
||||
def get_current(self):
|
||||
return self._current
|
||||
|
||||
def exists(self, identifier):
|
||||
if identifier is None:
|
||||
return False
|
||||
elif identifier == "_default":
|
||||
return True
|
||||
else:
|
||||
path = self._get_profile_path(identifier)
|
||||
return os.path.exists(path) and os.path.isfile(path)
|
||||
|
||||
def _load_all(self):
|
||||
all_identifiers = self._load_all_identifiers()
|
||||
results = dict()
|
||||
for identifier, path in all_identifiers.items():
|
||||
if identifier == "_default":
|
||||
profile = self._load_default()
|
||||
else:
|
||||
profile = self._load_from_path(path)
|
||||
|
||||
if profile is None:
|
||||
continue
|
||||
|
||||
results[identifier] = profile
|
||||
return results
|
||||
|
||||
def _load_all_identifiers(self):
|
||||
results = dict(_default=None)
|
||||
for entry in os.listdir(self._folder):
|
||||
if entry.startswith(".") or not entry.endswith(".profile") or entry == "_default.profile":
|
||||
continue
|
||||
|
||||
path = os.path.join(self._folder, entry)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
|
||||
identifier = entry[:-len(".profile")]
|
||||
results[identifier] = path
|
||||
return results
|
||||
|
||||
def _load_from_path(self, path):
|
||||
if not os.path.exists(path) or not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
import yaml
|
||||
with open(path) as f:
|
||||
profile = yaml.safe_load(f)
|
||||
return profile
|
||||
|
||||
def _save_to_path(self, path, profile, allow_overwrite=False):
|
||||
if os.path.exists(path) and not allow_overwrite:
|
||||
raise SaveError("Profile %s already exists and not allowed to overwrite" % profile["id"])
|
||||
|
||||
import yaml
|
||||
with open(path, "wb") as f:
|
||||
try:
|
||||
yaml.safe_dump(profile, f)
|
||||
except Exception as e:
|
||||
raise SaveError("Cannot save profile %s: %s" % (profile["id"], e.message))
|
||||
|
||||
def _remove_from_path(self, path):
|
||||
try:
|
||||
os.remove(path)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def _load_default(self):
|
||||
default_profile = settings().get(["printerProfiles", "defaultProfile"])
|
||||
return dict_merge(copy.deepcopy(self.__class__.default), default_profile)
|
||||
|
||||
def _get_profile_path(self, identifier):
|
||||
return os.path.join(self._folder, "%s.profile" % identifier)
|
||||
|
||||
def _sanitize(self, name):
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
if "/" in name or "\\" in name:
|
||||
raise ValueError("name must not contain / or \\")
|
||||
|
||||
import string
|
||||
valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits)
|
||||
sanitized_name = ''.join(c for c in name if c in valid_chars)
|
||||
sanitized_name = sanitized_name.replace(" ", "_")
|
||||
return sanitized_name
|
||||
|
|
@ -27,6 +27,7 @@ babel = Babel(app)
|
|||
debug = False
|
||||
|
||||
printer = None
|
||||
printerProfileManager = None
|
||||
fileManager = None
|
||||
slicingManager = None
|
||||
analysisQueue = None
|
||||
|
|
@ -42,6 +43,7 @@ user_permission = Permission(RoleNeed("user"))
|
|||
|
||||
# only import the octoprint stuff down here, as it might depend on things defined above to be initialized already
|
||||
from octoprint.printer import Printer, getConnectionOptions
|
||||
from octoprint.printer.profile import PrinterProfileManager
|
||||
from octoprint.settings import settings
|
||||
import octoprint.users as users
|
||||
import octoprint.events as events
|
||||
|
|
@ -200,6 +202,7 @@ class Server():
|
|||
self._checkForRoot()
|
||||
|
||||
global printer
|
||||
global printerProfileManager
|
||||
global fileManager
|
||||
global slicingManager
|
||||
global analysisQueue
|
||||
|
|
@ -226,13 +229,14 @@ class Server():
|
|||
# then initialize the plugin manager
|
||||
pluginManager = octoprint.plugin.plugin_manager(init=True)
|
||||
|
||||
printerProfileManager = PrinterProfileManager()
|
||||
eventManager = events.eventManager()
|
||||
analysisQueue = octoprint.filemanager.analysis.AnalysisQueue()
|
||||
slicingManager = octoprint.slicing.SlicingManager(settings().getBaseFolder("slicingProfiles"))
|
||||
slicingManager = octoprint.slicing.SlicingManager(settings().getBaseFolder("slicingProfiles"), printerProfileManager)
|
||||
storage_managers = dict()
|
||||
storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(settings().getBaseFolder("uploads"))
|
||||
fileManager = octoprint.filemanager.FileManager(analysisQueue, slicingManager, initial_storage_managers=storage_managers)
|
||||
printer = Printer(fileManager, analysisQueue)
|
||||
printer = Printer(fileManager, analysisQueue, printerProfileManager)
|
||||
appSessionManager = util.flask.AppSessionManager()
|
||||
|
||||
# configure additional template folders for jinja2
|
||||
|
|
@ -341,9 +345,10 @@ class Server():
|
|||
eventManager.fire(events.Events.STARTUP)
|
||||
if settings().getBoolean(["serial", "autoconnect"]):
|
||||
(port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"])
|
||||
printer_profile = printerProfileManager.get_default()
|
||||
connectionOptions = getConnectionOptions()
|
||||
if port in connectionOptions["ports"]:
|
||||
printer.connect(port, baudrate)
|
||||
printer.connect(port=port, baudrate=baudrate, profile=printer_profile["id"] if "id" in printer_profile else "_default")
|
||||
|
||||
# start up watchdogs
|
||||
observer = Observer()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ from . import timelapse as api_timelapse
|
|||
from . import users as api_users
|
||||
from . import log as api_logs
|
||||
from . import slicing as api_slicing
|
||||
from . import printer_profiles as api_printer_profiles
|
||||
|
||||
|
||||
VERSION = "0.1"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from flask import request, jsonify, make_response
|
|||
|
||||
from octoprint.settings import settings
|
||||
from octoprint.printer import getConnectionOptions
|
||||
from octoprint.server import printer, NO_CONTENT
|
||||
from octoprint.server import printer, printerProfileManager, NO_CONTENT
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
import octoprint.util as util
|
||||
|
|
@ -17,13 +17,15 @@ import octoprint.util as util
|
|||
|
||||
@api.route("/connection", methods=["GET"])
|
||||
def connectionState():
|
||||
state, port, baudrate = printer.getCurrentConnection()
|
||||
state, port, baudrate, printer_profile = printer.getCurrentConnection()
|
||||
current = {
|
||||
"state": state,
|
||||
"port": port,
|
||||
"baudrate": baudrate
|
||||
"baudrate": baudrate,
|
||||
"printerProfile": printer_profile["id"] if printer_profile is not None and "id" in printer_profile else "_default"
|
||||
}
|
||||
return jsonify({"current": current, "options": getConnectionOptions()})
|
||||
|
||||
return jsonify({"current": current, "options": _get_options()})
|
||||
|
||||
|
||||
@api.route("/connection", methods=["POST"])
|
||||
|
|
@ -39,28 +41,48 @@ def connectionCommand():
|
|||
return response
|
||||
|
||||
if command == "connect":
|
||||
options = getConnectionOptions()
|
||||
connection_options = getConnectionOptions()
|
||||
|
||||
port = None
|
||||
baudrate = None
|
||||
printerProfile = None
|
||||
if "port" in data.keys():
|
||||
port = data["port"]
|
||||
if port not in options["ports"]:
|
||||
if port not in connection_options["ports"]:
|
||||
return make_response("Invalid port: %s" % port, 400)
|
||||
if "baudrate" in data.keys():
|
||||
baudrate = data["baudrate"]
|
||||
if baudrate not in options["baudrates"]:
|
||||
if baudrate not in connection_options["baudrates"]:
|
||||
return make_response("Invalid baudrate: %d" % baudrate, 400)
|
||||
if "printerProfile" in data.keys():
|
||||
printerProfile = data["printerProfile"]
|
||||
if not printerProfileManager.exists(printerProfile):
|
||||
return make_response("Invalid printer profile: %s" % printerProfile, 400)
|
||||
if "save" in data.keys() and data["save"]:
|
||||
settings().set(["serial", "port"], port)
|
||||
settings().setInt(["serial", "baudrate"], baudrate)
|
||||
printerProfileManager.set_default(printerProfile)
|
||||
if "autoconnect" in data.keys():
|
||||
settings().setBoolean(["serial", "autoconnect"], data["autoconnect"])
|
||||
settings().save()
|
||||
printer.connect(port=port, baudrate=baudrate)
|
||||
printer.connect(port=port, baudrate=baudrate, profile=printerProfile)
|
||||
elif command == "disconnect":
|
||||
printer.disconnect()
|
||||
|
||||
return NO_CONTENT
|
||||
|
||||
def _get_options():
|
||||
connection_options = getConnectionOptions()
|
||||
profile_options = printerProfileManager.get_all()
|
||||
default_profile = printerProfileManager.get_default()
|
||||
|
||||
options = dict(
|
||||
ports=connection_options["ports"],
|
||||
baudrates=connection_options["baudrates"],
|
||||
printerProfiles=[dict(id=printer_profile["id"], name=printer_profile["name"] if "name" in printer_profile else printer_profile["id"]) for printer_profile in profile_options.values() if "id" in printer_profile],
|
||||
portPreference=connection_options["portPreference"],
|
||||
baudratePreference=connection_options["baudratePreference"],
|
||||
printerProfilePreference=default_profile["id"] if "id" in default_profile else None
|
||||
)
|
||||
|
||||
return options
|
||||
108
src/octoprint/server/api/printer_profiles.py
Normal file
108
src/octoprint/server/api/printer_profiles.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
from flask import jsonify, make_response, request, url_for
|
||||
|
||||
from octoprint.server.api import api, NO_CONTENT
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
from octoprint.util import dict_merge
|
||||
|
||||
from octoprint.server import printerProfileManager
|
||||
|
||||
|
||||
@api.route("/printerProfiles", methods=["GET"])
|
||||
def printerProfilesList():
|
||||
all_profiles = printerProfileManager.get_all()
|
||||
return jsonify(dict(profiles=all_profiles.values()))
|
||||
|
||||
@api.route("/printerProfiles", methods=["POST"])
|
||||
def printerProfilesAdd():
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return None, None, make_response("Expected content-type JSON", 400)
|
||||
|
||||
json_data = request.json
|
||||
if not "profile" in json_data:
|
||||
return None, None, make_response("No profile included in request", 400)
|
||||
|
||||
base_profile = printerProfileManager.get_default()
|
||||
if "basedOn" in json_data and isinstance(json_data["basedOn"], basestring):
|
||||
other_profile = printerProfileManager.get(json_data["basedOn"])
|
||||
if other_profile is not None:
|
||||
base_profile = other_profile
|
||||
|
||||
if "id" in base_profile:
|
||||
del base_profile["id"]
|
||||
if "name" in base_profile:
|
||||
del base_profile["name"]
|
||||
profile = dict_merge(base_profile, json_data["profile"])
|
||||
|
||||
if not "id" in profile and not "name" in profile:
|
||||
return None, None, make_response("Profile must contain either id or name")
|
||||
elif not "name" in profile:
|
||||
return None, None, make_response("Profile must contain a name")
|
||||
|
||||
try:
|
||||
saved_profile = printerProfileManager.save(profile, allow_overwrite=False)
|
||||
except Exception as e:
|
||||
return None, None, make_response("Could not save profile: %s" % e.message)
|
||||
|
||||
return jsonify(dict(profile=_convert_profile(saved_profile)))
|
||||
|
||||
@api.route("/printerProfiles/<string:identifier>", methods=["GET"])
|
||||
def printerProfilesGet(identifier):
|
||||
profile = printerProfileManager.get(identifier)
|
||||
if profile is None:
|
||||
make_response("Unknown profile: %s" % identifier, 404)
|
||||
|
||||
@api.route("/printerProfiles/<string:identifier>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def printerProfilesDelete(identifier):
|
||||
printerProfileManager.remove(identifier)
|
||||
return NO_CONTENT
|
||||
|
||||
@api.route("/printerProfiles/<string:identifier>", methods=["PATCH"])
|
||||
@restricted_access
|
||||
def printerProfilesUpdate(identifier ):
|
||||
if not "application/json" in request.headers["Content-Type"]:
|
||||
return None, None, make_response("Expected content-type JSON", 400)
|
||||
|
||||
json_data = request.json
|
||||
if not "profile" in json_data:
|
||||
return None, None, make_response("No profile included in request", 400)
|
||||
|
||||
profile = printerProfileManager.get(identifier)
|
||||
if profile is None:
|
||||
profile = printerProfileManager.get_default()
|
||||
|
||||
new_profile = json_data["profile"]
|
||||
new_profile = dict_merge(profile, new_profile)
|
||||
|
||||
new_profile["id"] = identifier
|
||||
try:
|
||||
saved_profile = printerProfileManager.save(new_profile, allow_overwrite=True)
|
||||
except Exception as e:
|
||||
return None, None, make_response("Could not save profile: %s" % e.message)
|
||||
|
||||
return jsonify(dict(profile=_convert_profile(saved_profile)))
|
||||
|
||||
def _convert_profiles(profiles):
|
||||
result = dict()
|
||||
for identifier, profile in profiles.items():
|
||||
result[identifier] = _convert_profile(profile)
|
||||
return result
|
||||
|
||||
def _convert_profile(profile, default=None):
|
||||
if default is None:
|
||||
default = printerProfileManager.get_default()["id"]
|
||||
|
||||
converted = copy.deepcopy(profile)
|
||||
converted["resource"] = url_for(".printerProfilesGet", identifier=profile["id"], _external=True)
|
||||
converted["default"] = (profile["id"] == default)
|
||||
return converted
|
||||
|
|
@ -94,7 +94,8 @@ default_settings = {
|
|||
"virtualSd": None,
|
||||
"watched": None,
|
||||
"plugins": None,
|
||||
"slicingProfiles": None
|
||||
"slicingProfiles": None,
|
||||
"printerProfiles": None
|
||||
},
|
||||
"temperature": {
|
||||
"profiles": [
|
||||
|
|
@ -102,6 +103,10 @@ default_settings = {
|
|||
{"name": "PLA", "extruder" : 180, "bed" : 60 }
|
||||
]
|
||||
},
|
||||
"printerProfiles": {
|
||||
"default": None,
|
||||
"defaultProfile": {}
|
||||
},
|
||||
"printerParameters": {
|
||||
"movementSpeed": {
|
||||
"x": 6000,
|
||||
|
|
|
|||
|
|
@ -49,8 +49,9 @@ class SlicingCancelled(BaseException):
|
|||
|
||||
|
||||
class SlicingManager(object):
|
||||
def __init__(self, profile_path):
|
||||
def __init__(self, profile_path, printer_profile_manager):
|
||||
self._profile_path = profile_path
|
||||
self._printer_profile_manager = printer_profile_manager
|
||||
|
||||
self._slicers = dict()
|
||||
self._slicer_names = dict()
|
||||
|
|
@ -94,7 +95,7 @@ class SlicingManager(object):
|
|||
def get_slicer(self, slicer, require_configured=True):
|
||||
return self._slicers[slicer] if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()) else None
|
||||
|
||||
def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None):
|
||||
def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None, printer_profile_id=None):
|
||||
if callback_args is None:
|
||||
callback_args = ()
|
||||
if callback_kwargs is None:
|
||||
|
|
@ -111,12 +112,20 @@ class SlicingManager(object):
|
|||
|
||||
slicer = self.get_slicer(slicer_name)
|
||||
|
||||
def slicer_worker(slicer, model_path, machinecode_path, profile_name, overrides, callback, callback_args, callback_kwargs):
|
||||
printer_profile = None
|
||||
if printer_profile_id is not None:
|
||||
printer_profile = self._printer_profile_manager.get(printer_profile_id)
|
||||
|
||||
if printer_profile is None:
|
||||
printer_profile = self._printer_profile_manager.get_current_or_default()["id"]
|
||||
|
||||
def slicer_worker(slicer, model_path, machinecode_path, profile_name, overrides, printer_profile, callback, callback_args, callback_kwargs):
|
||||
try:
|
||||
slicer_name = slicer.get_slicer_properties()["type"]
|
||||
with self.temporary_profile(slicer_name, name=profile_name, overrides=overrides) as profile_path:
|
||||
ok, result = slicer.do_slice(
|
||||
model_path,
|
||||
printer_profile,
|
||||
machinecode_path=machinecode_path,
|
||||
profile_path=profile_path,
|
||||
on_progress=on_progress,
|
||||
|
|
@ -133,7 +142,7 @@ class SlicingManager(object):
|
|||
|
||||
import threading
|
||||
slicer_worker_thread = threading.Thread(target=slicer_worker,
|
||||
args=(slicer, source_path, dest_path, profile_name, overrides, callback, callback_args, callback_kwargs))
|
||||
args=(slicer, source_path, dest_path, profile_name, overrides, printer_profile_id, callback, callback_args, callback_kwargs))
|
||||
slicer_worker_thread.daemon = True
|
||||
slicer_worker_thread.start()
|
||||
return True, None
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -76,7 +76,7 @@ $(function() {
|
|||
var temperatureViewModel = new TemperatureViewModel(loginStateViewModel, settingsViewModel);
|
||||
var controlViewModel = new ControlViewModel(loginStateViewModel, settingsViewModel);
|
||||
var terminalViewModel = new TerminalViewModel(loginStateViewModel, settingsViewModel);
|
||||
var slicingViewModel = new SlicingViewModel(loginStateViewModel);
|
||||
var slicingViewModel = new SlicingViewModel(loginStateViewModel, connectionViewModel);
|
||||
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel, slicingViewModel);
|
||||
var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel);
|
||||
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
|
||||
self.portOptions = ko.observableArray(undefined);
|
||||
self.baudrateOptions = ko.observableArray(undefined);
|
||||
self.printerOptions = ko.observableArray(undefined);
|
||||
self.selectedPort = ko.observable(undefined);
|
||||
self.selectedBaudrate = ko.observable(undefined);
|
||||
self.selectedPrinter = ko.observable(undefined);
|
||||
self.saveSettings = ko.observable(undefined);
|
||||
self.autoconnect = ko.observable(undefined);
|
||||
|
||||
|
|
@ -24,7 +26,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
return gettext("Connect");
|
||||
else
|
||||
return gettext("Disconnect");
|
||||
})
|
||||
});
|
||||
|
||||
self.previousIsOperational = undefined;
|
||||
|
||||
|
|
@ -37,32 +39,37 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
self.fromResponse(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
var ports = response.options.ports;
|
||||
var baudrates = response.options.baudrates;
|
||||
var printerProfiles = response.options.printerProfiles;
|
||||
var portPreference = response.options.portPreference;
|
||||
var baudratePreference = response.options.baudratePreference;
|
||||
var printerPreference = response.options.printerProfilePreference;
|
||||
|
||||
self.portOptions(ports);
|
||||
self.baudrateOptions(baudrates);
|
||||
self.printerOptions(printerProfiles);
|
||||
|
||||
if (!self.selectedPort() && ports && ports.indexOf(portPreference) >= 0)
|
||||
self.selectedPort(portPreference);
|
||||
if (!self.selectedBaudrate() && baudrates && baudrates.indexOf(baudratePreference) >= 0)
|
||||
self.selectedBaudrate(baudratePreference);
|
||||
if (!self.selectedPrinter() && printerProfiles && printerProfiles.indexOf(printerPreference) >= 0)
|
||||
self.selectedPrinter(printerPreference);
|
||||
|
||||
self.saveSettings(false);
|
||||
}
|
||||
};
|
||||
|
||||
self.fromHistoryData = function(data) {
|
||||
self._processStateData(data.state);
|
||||
}
|
||||
};
|
||||
|
||||
self.fromCurrentData = function(data) {
|
||||
self._processStateData(data.state);
|
||||
}
|
||||
};
|
||||
|
||||
self._processStateData = function(data) {
|
||||
self.previousIsOperational = self.isOperational();
|
||||
|
|
@ -85,7 +92,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
connectionTab.collapse("show");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.connect = function() {
|
||||
if (self.isErrorOrClosed()) {
|
||||
|
|
@ -93,6 +100,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
"command": "connect",
|
||||
"port": self.selectedPort(),
|
||||
"baudrate": self.selectedBaudrate(),
|
||||
"printerProfile": self.selectedPrinter(),
|
||||
"autoconnect": self.settings.serial_autoconnect()
|
||||
};
|
||||
|
||||
|
|
@ -119,7 +127,7 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
|
|||
data: JSON.stringify({"command": "disconnect"})
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.onStartup = function() {
|
||||
self.requestData();
|
||||
|
|
|
|||
144
src/octoprint/static/js/app/viewmodels/printerprofiles.js
Normal file
144
src/octoprint/static/js/app/viewmodels/printerprofiles.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
function PrinterProfilesViewModel() {
|
||||
var self = this;
|
||||
|
||||
self.selected = ko.observable();
|
||||
self.default = ko.observable();
|
||||
self.available = ko.observableArray();
|
||||
self.currentProfile = ko.computed(function() {
|
||||
var currentProfile = undefined;
|
||||
var defaultProfile = undefined;
|
||||
|
||||
_.each(self.available(), function(profile) {
|
||||
if (profile.id() == self.selected()) {
|
||||
currentProfile = profile;
|
||||
}
|
||||
if (profile.id() == self.default()) {
|
||||
defaultProfile = profile;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentProfile != undefined) {
|
||||
return currentProfile;
|
||||
} else {
|
||||
return defaultProfile;
|
||||
}
|
||||
});
|
||||
|
||||
self.editorName = ko.observable();
|
||||
self.editorColor = ko.observable();
|
||||
self.editorIdentifier = ko.observable();
|
||||
|
||||
self.editorWidth = ko.observable();
|
||||
self.editorDepth = ko.observable();
|
||||
self.editorHeight = ko.observable();
|
||||
self.editorFormFactor = ko.observable();
|
||||
|
||||
self.editorHeatedBed = ko.observable();
|
||||
|
||||
self.editorExtruders = ko.observable();
|
||||
self.editorExtruderOffsets = ko.observableArray();
|
||||
|
||||
self.editorAxisXSpeed = ko.observable();
|
||||
self.editorAxisYSpeed = ko.observable();
|
||||
self.editorAxisZSpeed = ko.observable();
|
||||
self.editorAxisESpeed = ko.observable();
|
||||
|
||||
self.editorAxisXInverted = ko.observable(false);
|
||||
self.editorAxisYInverted = ko.observable(false);
|
||||
self.editorAxisZInverted = ko.observable(false);
|
||||
|
||||
self.requestData = function() {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "printerProfiles",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
})
|
||||
};
|
||||
|
||||
self.fromResponse = function(data) {
|
||||
self.selectedProfile(data.current);
|
||||
self.defaultProfile(data.default);
|
||||
self.availableProfiles(data.profiles);
|
||||
};
|
||||
|
||||
self.addProfile = function() {
|
||||
var profile = self._editorData();
|
||||
$.ajax({
|
||||
url: API_BASEURL + "printerProfiles/" + profile.id,
|
||||
type: "PUT",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify({profile: profile})
|
||||
});
|
||||
};
|
||||
|
||||
self.removeProfile = function(data) {
|
||||
$.ajax({
|
||||
url: data.resource,
|
||||
type: "DELETE",
|
||||
dataType: "json"
|
||||
})
|
||||
};
|
||||
|
||||
self.updateProfile = function(identifier, profile) {
|
||||
if (profile == undefined) {
|
||||
profile = self._editorData();
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: API_BASEURL + "printerProfiles/" + profile.id,
|
||||
type: "PATCH",
|
||||
dataType: "json",
|
||||
contentType: "application/json; charset=UTF-8",
|
||||
data: JSON.stringify({profile: profile})
|
||||
});
|
||||
};
|
||||
|
||||
self._editorData = function() {
|
||||
var profile = {
|
||||
name: self.editorName(),
|
||||
color: self.editorColor(),
|
||||
id: self.editorIdentifier(),
|
||||
volume: {
|
||||
width: self.editorWidth(),
|
||||
depth: self.editorDepth(),
|
||||
height: self.editorHeight(),
|
||||
type: self.editorFormFactor()
|
||||
},
|
||||
heatedBed: self.editorHeatedBed(),
|
||||
extruder: {
|
||||
count: self.editorExtruders(),
|
||||
offsets: [
|
||||
[0.0, 0.0]
|
||||
]
|
||||
},
|
||||
axes: {
|
||||
x: {
|
||||
speed: self.editorAxisXSpeed(),
|
||||
inverted: self.editorAxisXInverted()
|
||||
},
|
||||
y: {
|
||||
speed: self.editorAxisYSpeed(),
|
||||
inverted: self.editorAxisYInverted()
|
||||
},
|
||||
z: {
|
||||
speed: self.editorAxisZSpeed(),
|
||||
inverted: self.editorAxisZInverted()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (self.editorExtruders() > 1) {
|
||||
for (var i = 1; i < self.editorExtruders(); i++) {
|
||||
var offset = [0.0, 0.0];
|
||||
if (i < self.editorExtruderOffsets().length) {
|
||||
offset = self.editorExtruderOffsets()[i];
|
||||
}
|
||||
profile.extruder.offsets.push(offset);
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
function SlicingViewModel(loginStateViewModel) {
|
||||
function SlicingViewModel(loginStateViewModel, connectionViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = loginStateViewModel;
|
||||
self.connection = connectionViewModel;
|
||||
|
||||
self.target = undefined;
|
||||
self.file = undefined;
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ ul.dropdown-menu li a {
|
|||
|
||||
/** Connection settings */
|
||||
|
||||
#connection_ports, #connection_baudrates {
|
||||
#connection_ports, #connection_baudrates, #connection_printers {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@
|
|||
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
|
||||
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Baudrate') }}</label>
|
||||
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
|
||||
<label for="connection_printers" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Printer Profile') }}</label>
|
||||
<select id="connection_printers" data-bind="options: printerOptions, optionsText: 'name', optionsValue: 'id', value: selectedPrinter, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Save connection settings') }}
|
||||
</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue