diff --git a/run b/run index 86f1fdf8..36b965d1 100755 --- a/run +++ b/run @@ -19,7 +19,7 @@ I don't know anything about). sys.path.insert(0, os.path.join(basedir, "src")) #import sys -#sys.path.append('/home/teja/Downloads/pyvmmonitor/public_api') +#sys.path.append('/home/teja/opt/pyvmmonitor/public_api') #import pyvmmonitor #pyvmmonitor.connect() diff --git a/src/octoprint/events.py b/src/octoprint/events.py index f91720a7..4bf3a7a0 100644 --- a/src/octoprint/events.py +++ b/src/octoprint/events.py @@ -86,9 +86,10 @@ class Events(object): # Settings SETTINGS_UPDATED = "SettingsUpdated" - + # GRBL LIMITS_HIT = "LimitsHit" + SOFT_RESET = "Soft-Reset" RT_STATE = "RealTimeState" diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index 5e232d59..468c6d51 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -204,23 +204,7 @@ class FileManager(object): source_meta = self.get_metadata(source_location, source_path) hash = source_meta["hash"] -#<<<<<<< HEAD -# class Wrapper(object): -# def __init__(self, stl_name, temp_path, hash): -# self.stl_name = stl_name -# self.temp_path = temp_path -# self.hash = hash -# -# def save(self, absolute_dest_path): -# with open(absolute_dest_path, "w") as d: -# d.write("; Generated from\n; {stl_name}\n; {hash}\r".format(**vars(self))) -# with open(tmp_path, "r") as s: -# import shutil -# shutil.copyfileobj(s, d) -# -#======= import io -#>>>>>>> upstream/maintenance links = [("model", dict(name=source_path))] _, stl_name = self.split_path(source_location, source_path) file_obj = StreamWrapper(os.path.basename(dest_path), diff --git a/src/octoprint/plugins/lasercutterprofiles/__init__.py b/src/octoprint/plugins/lasercutterprofiles/__init__.py index e721e34e..7f7ed57e 100644 --- a/src/octoprint/plugins/lasercutterprofiles/__init__.py +++ b/src/octoprint/plugins/lasercutterprofiles/__init__.py @@ -204,15 +204,17 @@ class LaserCutterProfilesPlugin(octoprint.plugin.SettingsPlugin, if self._printer.is_locked() or self._printer.is_operational(): if "volume" in new_profile: if "width" in new_profile["volume"]: - width = int(new_profile['volume']['width']) + width = float(new_profile['volume']['width']) if identifier == "_mrbeam_senior": width *= 2 + width += float(new_profile['volume']['origin_offset_x']) self._printer.commands('$130=' + str(width)) time.sleep(0.1) ### TODO find better solution then sleep if "depth" in new_profile["volume"]: - depth = int(new_profile['volume']['depth']) + depth = float(new_profile['volume']['depth']) if identifier == "_mrbeam_senior": depth *= 2 + depth += float(new_profile['volume']['origin_offset_y']) self._printer.commands('$131=' + str(depth)) new_profile["id"] = identifier diff --git a/src/octoprint/plugins/lasercutterprofiles/profile.py b/src/octoprint/plugins/lasercutterprofiles/profile.py index a8109be6..e493de03 100644 --- a/src/octoprint/plugins/lasercutterprofiles/profile.py +++ b/src/octoprint/plugins/lasercutterprofiles/profile.py @@ -29,9 +29,11 @@ class LaserCutterProfileManager(object): name = "Mr Beam", model = "Junior", volume=dict( - width = 216, - depth = 297, + width = 217, + depth = 298, height = 0, + origin_offset_x = 1, + origin_offset_y = 1, ), zAxis = False, axes=dict( diff --git a/src/octoprint/plugins/lasercutterprofiles/static/js/lasercutterprofiles.js b/src/octoprint/plugins/lasercutterprofiles/static/js/lasercutterprofiles.js index f899a951..0e50ab67 100644 --- a/src/octoprint/plugins/lasercutterprofiles/static/js/lasercutterprofiles.js +++ b/src/octoprint/plugins/lasercutterprofiles/static/js/lasercutterprofiles.js @@ -16,7 +16,9 @@ $(function() { formFactor: "rectangular", width: 216, depth: 297, - height: 0 + height: 0, + origin_offset_x: 1, + origin_offset_y: 1 }, zAxis: false, axes: { @@ -108,8 +110,8 @@ $(function() { self.currentProfile(currentProfile); self.currentProfileData(currentProfileData); - self.workingarea.workingAreaWidthMM(self.currentProfileData().volume.width()); - self.workingarea.workingAreaHeightMM(self.currentProfileData().volume.depth()); + self.workingarea.workingAreaWidthMM(self.currentProfileData().volume.width() - self.currentProfileData().volume.origin_offset_x()); + self.workingarea.workingAreaHeightMM(self.currentProfileData().volume.depth() - self.currentProfileData().volume.origin_offset_y()); var maxSpeed = Math.min(self.currentProfileData().axes.x.speed(), self.currentProfileData().axes.y.speed()); self.conversion.maxSpeed(maxSpeed); }; diff --git a/src/octoprint/plugins/svgtogcode/__init__.py b/src/octoprint/plugins/svgtogcode/__init__.py index 41db0b16..123e8630 100644 --- a/src/octoprint/plugins/svgtogcode/__init__.py +++ b/src/octoprint/plugins/svgtogcode/__init__.py @@ -235,7 +235,7 @@ class SvgToGcodePlugin(octoprint.plugin.SlicerPlugin, def get_assets(self): return dict( - js=[ "js/convert.js", "js/working_area.js", "js/gcode_parser.js", "js/lib/snap.svg-min.js", "js/matrix_oven.js", "js/drag_scale_rotate.js"], + js=[ "js/convert.js", "js/working_area.js", "js/gcode_parser.js", "js/lib/snap.svg-min.js", "js/lib/photobooth_min.js", "js/matrix_oven.js", "js/render_fills.js", "js/drag_scale_rotate.js"], less=["less/svgtogcode.less"], css=["css/svgtogcode.css", "css/mrbeam.css"] ) diff --git a/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css b/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css index 471faf5a..a7a6e0a2 100644 --- a/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css +++ b/src/octoprint/plugins/svgtogcode/static/css/svgtogcode.css @@ -1,6 +1,6 @@ table th.settings_plugin_svgtogcode_profiles_key,table td.settings_plugin_svgtogcode_profiles_key{text-overflow:ellipsis;text-align:left;width:200px}table th.settings_plugin_svgtogcode_profiles_name,table td.settings_plugin_svgtogcode_profiles_name{text-overflow:ellipsis;text-align:left}table th.settings_plugin_svgtogcode_profiles_actions,table td.settings_plugin_svgtogcode_profiles_actions{text-align:center;width:100px}table th.settings_plugin_svgtogcode_profiles_actions a,table td.settings_plugin_svgtogcode_profiles_actions a{text-decoration:none;color:#000}table th.settings_plugin_svgtogcode_profiles_actions a.disabled,table td.settings_plugin_svgtogcode_profiles_actions a.disabled{color:#ccc;cursor:default} .slider_manual_input { - margin-left: 1.5em; + margin-left: 1.5em; width: 2.5em; } @@ -22,7 +22,7 @@ table th.settings_plugin_svgtogcode_profiles_key,table td.settings_plugin_svgtog } .svgtogcode_grayscale { - background-image: linear-gradient(90deg, #FFFFFF, #000000); + background-image: linear-gradient(90deg, #FFFFFF, #000000); width:220px; display: inline-block; } @@ -37,7 +37,7 @@ svg text { -ms-user-select: none; cursor: default; pointer-events: none; - + } .img_slider{ @@ -60,8 +60,8 @@ svg text { left:0; } -.img_preprocessing_preview.after .contrast, -.img_preprocessing_preview.after .sharpened, +.img_preprocessing_preview.after .contrast, +.img_preprocessing_preview.after .sharpened, .img_preprocessing_preview.after .sharpened_contrast { opacity: 0; } @@ -85,3 +85,20 @@ svg text { font-size: large; padding-right: .5em; } + +#photo_preview { + width: 400px; + height: 300px; + margin: auto; +} + +.overrideSlider { + margin-bottom: 8px; +} + +.overrideSlider input { + width:45%; +} +.overrideSlider span { + padding-left: .6em; +} diff --git a/src/octoprint/plugins/svgtogcode/static/js/convert.js b/src/octoprint/plugins/svgtogcode/static/js/convert.js index 34866713..9ba684cb 100644 --- a/src/octoprint/plugins/svgtogcode/static/js/convert.js +++ b/src/octoprint/plugins/svgtogcode/static/js/convert.js @@ -35,10 +35,15 @@ $(function(){ self.laserSpeed = ko.observable(undefined); self.maxSpeed = ko.observable(3000); self.minSpeed = ko.observable(20); + self.fill_areas = ko.observable(false); + self.show_fill_areas_checkbox = ko.observable(false); // image engraving stuff // preset values are a good start for wood engraving - self.show_image_parameters = ko.observable(false); + self.images_placed = ko.observable(false); + self.show_image_parameters = ko.computed(function(){ + return self.images_placed() || (self.fill_areas() && self.show_vector_parameters()); + }); self.imgIntensityWhite = ko.observable(0); self.imgIntensityBlack = ko.observable(500); self.imgFeedrateWhite = ko.observable(1500); @@ -76,12 +81,13 @@ $(function(){ // shows conversion dialog and extracts svg first self.show_conversion_dialog = function() { - self.svg = self.workingArea.getCompositionSVG(); self.gcodeFilesToAppend = self.workingArea.getPlacedGcodes(); - self.show_image_parameters(self.workingArea.getPlacedImages().length > 0); self.show_vector_parameters(self.workingArea.getPlacedSvgs().length > 0); - - if(self.svg !== undefined){ + self.show_fill_areas_checkbox(self.workingArea.hasFilledVectors()) + self.images_placed(self.workingArea.getPlacedImages().length > 0); + //self.show_image_parameters(self.workingArea.getPlacedImages().length > 0); + + if(self.show_vector_parameters() || self.show_image_parameters()){ if(self.laserIntensity() === undefined){ var intensity = self.settings.settings.plugins.svgtogcode.defaultIntensity(); self.laserIntensity(intensity); @@ -93,7 +99,7 @@ $(function(){ var gcodeFile = self.create_gcode_filename(self.workingArea.placedDesigns()); self.gcodeFilename(gcodeFile); - + self.title(gettext("Converting")); $("#dialog_vector_graphics_conversion").modal("show"); // calls self.convert() afterwards } else { @@ -101,6 +107,13 @@ $(function(){ self.convert(); } }; + + self.cancel_conversion = function(){ + if(self.slicing_in_progress()){ + console.log('cancel slicing', self.slicing_in_progress()); + // TODO cancel slicing properly + } + }; self.create_gcode_filename = function(placedDesigns){ if(placedDesigns.length > 0){ @@ -233,43 +246,48 @@ $(function(){ if(self.gcodeFilesToAppend.length === 1 && self.svg === undefined){ self.files.startGcodeWithSafetyWarning(self.gcodeFilesToAppend[0]); } else { - var filename = self.gcodeFilename() + self.settingsString() + '.gco'; - var gcodeFilename = self._sanitize(filename); + self.slicing_in_progress(true); + self.workingArea.getCompositionSVG(self.fill_areas(), function(composition){ + self.svg = composition; + var filename = self.gcodeFilename() + self.settingsString() + '.gco'; + var gcodeFilename = self._sanitize(filename); - var data = { - command: "convert", - "profile.speed": self.laserSpeed(), - "profile.intensity": self.laserIntensity(), - "profile.pierce_time": self.pierceTime(), - "profile.intensity_black" : self.imgIntensityBlack(), - "profile.intensity_white" : self.imgIntensityWhite(), - "profile.feedrate_black" : self.imgFeedrateBlack(), - "profile.feedrate_white" : self.imgFeedrateWhite(), - "profile.img_contrast" : self.imgContrast(), - "profile.img_sharpening" : self.imgSharpening(), - "profile.img_dithering" : self.imgDithering(), - "profile.beam_diameter" : self.beamDiameter(), - slicer: "svgtogcode", - gcode: gcodeFilename - }; + var data = { + command: "convert", + "profile.speed": self.laserSpeed(), + "profile.intensity": self.laserIntensity(), + "profile.fill_areas": self.fill_areas(), + "profile.pierce_time": self.pierceTime(), + "profile.intensity_black" : self.imgIntensityBlack(), + "profile.intensity_white" : self.imgIntensityWhite(), + "profile.feedrate_black" : self.imgFeedrateBlack(), + "profile.feedrate_white" : self.imgFeedrateWhite(), + "profile.img_contrast" : self.imgContrast(), + "profile.img_sharpening" : self.imgSharpening(), + "profile.img_dithering" : self.imgDithering(), + "profile.beam_diameter" : self.beamDiameter(), + slicer: "svgtogcode", + gcode: gcodeFilename + }; - if(self.svg !== undefined){ - data.svg = self.svg; - } else { - data.svg = ''; - } - if(self.gcodeFilesToAppend !== undefined){ - data.gcodeFilesToAppend = self.gcodeFilesToAppend; - } + if(self.svg !== undefined){ + data.svg = self.svg; + } else { + data.svg = ''; + } + if(self.gcodeFilesToAppend !== undefined){ + data.gcodeFilesToAppend = self.gcodeFilesToAppend; + } + + $.ajax({ + url: API_BASEURL + "files/convert", + type: "POST", + dataType: "json", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify(data) + }); - $.ajax({ - url: API_BASEURL + "files/convert", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify(data) }); - } }; @@ -291,29 +309,29 @@ $(function(){ }; self.onEventSlicingStarted = function(payload){ self.slicing_in_progress(true); - console.log("onSlicingDone" , payload); }; self.onEventSlicingDone = function(payload){ // payload -// gcode: "angelina_20091211_0193_11more_i1000s300.gco" +// gcode: "ex_11more_i1000s300.gco" // gcode_location: "local" -// stl: "local/angelina_jolie_20091211_0193_11more_i1000s300.svg" +// stl: "local/ex_11more_i1000s300.svg" // time: 30.612739086151123 self.gcodeFilename(undefined); self.svg = undefined; $("#dialog_vector_graphics_conversion").modal("hide"); self.slicing_in_progress(false); + //console.log("onSlicingDone" , payload); }; self.onEventSlicingCancelled = function(payload){ self.gcodeFilename(undefined); self.svg = undefined; self.slicing_in_progress(false); $("#dialog_vector_graphics_conversion").modal("hide"); - console.log("onSlicingCancelled" , payload); + //console.log("onSlicingCancelled" , payload); }; self.onEventSlicingFailed = function(payload){ self.slicing_in_progress(false); - console.log("onSlicingFailed" , payload); + //console.log("onSlicingFailed" , payload); }; self._configureIntensitySlider = function() { diff --git a/src/octoprint/plugins/svgtogcode/static/js/drag_scale_rotate.js b/src/octoprint/plugins/svgtogcode/static/js/drag_scale_rotate.js index a23966b9..92e4f376 100644 --- a/src/octoprint/plugins/svgtogcode/static/js/drag_scale_rotate.js +++ b/src/octoprint/plugins/svgtogcode/static/js/drag_scale_rotate.js @@ -1,9 +1,8 @@ -// Matrix Oven - a snapsvg.io plugin to apply & remove transformations from svg files. +// Drag, Scale & Rotate - a snapsvg.io plugin to free transform objects in an svg. // Copyright (C) 2015 Teja Philipp // -// based on work by https://gist.github.com/timo22345/9413158 -// and https://github.com/duopixel/Method-Draw/blob/master/editor/src/svgcanvas.js -// +// heavily inspired by http://svg.dabbles.info +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the @@ -22,7 +21,7 @@ Snap.plugin(function (Snap, Element, Paper, global) { /** - * bakes transformations of the element and all sub-elements into coordinates + * * * @returns {undefined} */ diff --git a/src/octoprint/plugins/svgtogcode/static/js/lib/photobooth_min.js b/src/octoprint/plugins/svgtogcode/static/js/lib/photobooth_min.js new file mode 100644 index 00000000..5a0d2ae6 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/static/js/lib/photobooth_min.js @@ -0,0 +1,22 @@ +/** +* +* Photobooth.js version 0.7 +* +* build Thu Oct 17 2013 16:43:37 GMT-0700 (Pacific Daylight Time) +* +* CSS +*/ +window.addEventListener("load",function(){var s = document.createElement("style"); s.innerHTML=".photobooth{position:relative;font:11px arial,sans-serif;overflow:hidden;user-select:none;-webkit-user-select:none;-moz-user-select:none;-o-user-select:none}.photobooth canvas{position:absolute;left:0;top:0}.photobooth .blind{position:absolute;left:0;top:0;opacity:0;width:100%;height:100%;background:#fff;z-index:1}.photobooth .blind.anim{transition:opacity 1500ms ease-out;-o-transition:opacity 1500ms ease-out;-moz-transition:opacity 1500ms ease-out;-webkit-transition:opacity 1500ms ease-out}.photobooth .warning{position:absolute;top:45%;background:#ffebeb;color:#cf0000;border:1px solid #cf0000;width:60%;left:50%;margin-left:-30%;display:none;padding:5px;z-index:10;text-align:center}.photobooth .warning span{text-decoration:underline;cursor:pointer;color:#333}.photobooth ul{width:30px;position:absolute;right:0;top:0;background:rgba( 0,0,0,.6 );height:190px;z-index:2;border-bottom-left-radius:5px}.photobooth ul li{width:30px;height:38px;background-repeat:no-repeat;background-position:center center;cursor:pointer;position:relative}.photobooth ul li:hover{background-color:#aaa}.photobooth ul li.selected{background-color:#ccc}.photobooth ul.noHSB{height:80px}.photobooth ul.noHSB li.hue,.photobooth ul.noHSB li.saturation,.photobooth ul.noHSB li.brightness{display:none}.photobooth ul li.hue{background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgACAAYAwERAAIRAQMRAf/EAHgAAQEAAAAAAAAAAAAAAAAAAAkIAQEAAwAAAAAAAAAAAAAAAAAKBggLEAAAAwQLAAAAAAAAAAAAAAAAMQZBAjQ4A3MEdMQFdQcICTkRAAEBBAcGBwAAAAAAAAAAABExAAEhElECMjMEBQlhwgNzFDgVNRY3CBgK/9oADAMBAAIRAxEAPwBGOKPmqmNdT5FD2YgarLO67OVueIqrxF2tI/1Kn0jjjKfFcJZEt+5BAUCAaKuw+ThT3vC0wbFof+U4Dnv3WGl8Pu47A8vecwabKy8ZRVNKFdF3dY72fztbVdFu67axelcfrPkYlPTutCW7qqYCkwDf/9k=)}.photobooth ul li.saturation{background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgACAAYAwERAAIRAQMRAf/EAGMAAAMAAAAAAAAAAAAAAAAAAAYICQEBAQEAAAAAAAAAAAAAAAAACAkKEAAABgMBAAAAAAAAAAAAAAAAwYIDMwZxAkQHEQABAgUFAAAAAAAAAAAAAAAAAQYxgQIyM3HBQgMH/9oADAMBAAIRAxEAPwAwo0rWdSFXHBYpnLZmWjVB/fLedIODu5Do81j1y2KE0CJlJA2uK5ZjtY2Kg//Z)}.photobooth ul li.brightness{background-image:url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgACAAYAwERAAIRAQMRAf/EAFcAAQAAAAAAAAAAAAAAAAAAAAoBAQAAAAAAAAAAAAAAAAAAAAAQAAAEBQUAAAAAAAAAAAAAAACxAwgBMXECBXJzBDQ1EQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwAcTWfR4GtIwC5mITxNUDgAYA0joY3aRKwB/9k=)}.photobooth ul li.crop{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAICAYAAADjoT9jAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEFJREFUeNpi/A8EDAjACMT/qUgzMCJZwMhAXQA2l4VGhsPNZKKR4XBfMMG8QiPASDcf0MIX/2FxgCJARRoMAAIMAK49Iv4yTUj5AAAAAElFTkSuQmCC)}.photobooth ul li.trigger{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAASCAYAAABB7B6eAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAa9JREFUeNqc1M8rRFEUwPF5M4MhP8aPIiIS21lQk1Is5ceChZIdOytlI/+A7Ig/gGRhpYiNbKQsrBRFLPzYWJghNH7MjOd76qhr8m6vOfWpmffevefec987juu6AZ8RQhhBpJHJuT+CfsiEDo6wGjYeKMKn8b8Um/jCG2qQ0skjyOIWB9hFNyaN8bWSwGEHM5q9EVc6mUQ9YpjDHQbwoQkjuspDDKNEF9hjJDjFcoAEx653XEoJMYoVxNGBGPZRhzbL+HTYWLEtpO6V6EQ5kijTc7HFiwyssDwgyXsxhW8tkZSxAAksoj3n7P4G20hatviKE3RpqXKN4V5K4TE+IQ89WBI8ao0DFkP49krw+057xbyWxBY72LIdXsbjnlzf8/kRbtgSeO1APqonnwlu8tlBIYp9JojmkyCiX7Kf6MsngcSsvvO2aMZEPmcgEcea7ua/aNKGaC2RY0lwgTNsYwwNOlkrprGOJe2q/84vvegabdrrQyqomrSTyirHtbPKc+84x4L2qBazORi/s9KuC7QfBY3JC1UVBlGt16PallPap+Tas+7wWc8za1Ql8yPAAAzkXGo1lmDtAAAAAElFTkSuQmCC)}.photobooth .submenu{background:rgba( 0,0,0,.6 );position:absolute;width:100px;opacity:0;height:20px;padding:5px 10px;color:#fff;top:4px;left:-124px;border-radius:5px;-webkit-transition:opacity 500ms ease;-moz-transition:opacity 500ms ease;-o-transition:opacity 500ms ease;-msie-transition:opacity 500ms ease;transition:opacity 500ms ease}.photobooth li:hover .submenu{opacity:1}.photobooth .submenu .tip{width:4px;height:8px;position:absolute;right:-4px;top:50%;margin-top:-2px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAICAYAAADeM14FAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADVJREFUeNpiYGBgmAnEDP///wdjJgYImMnIyAhmwATggowwLTCArAKrQDqyQDrcMGQlAAEGAAGOCdflbyWyAAAAAElFTkSuQmCC)}.photobooth .submenu .slider{width:100px;height:20px;position:relative}.photobooth .submenu .slider .track{height:2px;width:100px;position:absolute;top:9px;background:rgba(255,255,255,.6)}.photobooth .submenu .slider .handle{height:14px;width:2px;position:absolute;top:3;background:#fff;z-index:2}.photobooth .submenu .slider .handle div{position:absolute;z-index:3;width:20px;top:-3px;height:20px;cursor:w-resize;left:-9px}.resizehandle{position:absolute;z-index:1;width:100px;height:100px;left:30px;top:30px;cursor:move;outline:1500px solid rgba( 0,0,0,.35 );box-shadow:2px 2px 10px rgba(0,0,0,.5),0 0 3px #000;opacity:0;transition:opacity 500ms ease;-moz-transition:opacity 500ms ease;-o-transition:opacity 500ms ease;-webkit-transition:opacity 500ms ease}noindex:-o-prefocus,.resizehandle{outline:0!important}@-moz-document url-prefix(){.resizehandle{ box-shadow:none!important}}.resizehandle .handle{width:100%;height:100%;border:2px dashed #0da4d3;margin:-2px 0 0 -2px;z-index:3;position:relative}.resizehandle .handle div{width:18px;height:18px;position:absolute;right:-2px;bottom:-2px;z-index:4;cursor:se-resize;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHdJREFUeNpi/P//PwO5gIlcjXxLr/xnIlujsg7pNsM0AgEjE7kaSfIzusZ/d4n0M1aNxPgZWeMHC4RGIJuREV8847IRpBGvnwlpxBnPRGkEyYOcjYx5l1z+z3/8Pwij8NHlQWwUPxNrI4afSdUI9zNZGoF8gAADAOGvmx/e+CgVAAAAAElFTkSuQmCC);background-position:top left;background-repeat:no-repeat}"; document.head.appendChild(s);},false); +/** +* JS +*/ +Photobooth=function(e){e.length&&(e=e[0]);var t=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.oGetUserMedia||navigator.msieGetUserMedia||!1;this.onImage=function(){},this.getHueOffset=function(){return o},this.setHueOffset=function(e){v(e,"hue")&&(o=e)},this.getBrightnessOffset=function(){return a},this.setBrightnessOffset=function(e){v(e,"brightness")&&(a=e)},this.getSaturationOffset=function(){return u},this.setSaturationOffset=function(e){v(e,"saturation")&&(u=e)},this.pause=function(){l===!1&&(l=!0,c&&c.stop&&c.stop())},this.resume=function(){l===!0&&(l=!1,M())}, + this.destroy=function(){ + this.pause(), + e.removeChild(b) + }, + this.forceHSB=!1,this.isSupported=!!t,this.resize=function(e,t){if(e<200||t<200)throw"Error: Not enough space for Photobooth. Min height / width is 200 px";p=e,d=t,C.setMax(p,d),b.style.width=e+"px",b.style.height=t+"px",w.width=e,w.height=t,S.width=e,S.height=t,T.width=e,T.height=t};var n=function(e){e.stopPropagation(),e.cancelBubble=!0},r=function(e){this.startX=0,this.startY=0,e.addEventListener("mousedown",this,!1)};r.prototype.onStart=function(e,t){},r.prototype.onMove=function(e,t){},r.prototype.onStop=function(e,t){},r.prototype.handleEvent=function(e){this["fon"+e.type](e)},r.prototype.fonmousedown=function(e){e.preventDefault(),this.startX=e.clientX,this.startY=e.clientY,this.onStart(this.startX,this.startY),document.addEventListener("mousemove",this,!1),document.addEventListener("mouseup",this,!1)},r.prototype.fonmousemove=function(e){this.onMove(e.clientX-this.startX,e.clientY-this.startY)},r.prototype.fonmouseup=function(e){this.onStop(e.clientX-this.startX,e.clientY-this.startY),document.removeEventListener("mousemove",this),document.removeEventListener("mouseup",this)};var i=function(e,t){e.innerHTML='';var i=50,s=50,o=e.getElementsByClassName("handle")[0],u=e.getElementsByClassName("slider")[0],a=new r(o);a.onMove=function(e){f(i+e)},a.onStop=function(e){i=s};var f=function(e){e>0&&e<100&&(s=e,o.style.left=e+"px",t((e-50)/100))},l=function(e){f(e.layerX),i=s};u.addEventListener("click",l,!1),o.addEventListener("click",n,!1)},s=function(e,t,i){this.setMax=function(e,n){t=e,i=n},this.getData=function(){return{x:s,y:o,width:u,height:a}},this.isActive=function(){return p},this.toggle=function(){p===!1?(d.style.opacity=1,p=!0):(d.style.opacity=0,p=!1)};var s=30,o=30,u=100,a=100,f=30,l=30,c=100,h=100,p=!1,d=document.createElement("div");d.className="resizehandle",d.innerHTML='
',e.appendChild(d);var v=d.getElementsByTagName("div")[0],m=new r(v);m.onMove=function(e,n){s+e+u0&&(f=s+e,d.style.left=f+"px"),o+n+a0&&(l=o+n,d.style.top=l+"px")},m.onStop=function(){s=f,o=l};var g=d.getElementsByTagName("div")[1];g.addEventListener("mousedown",n,!1);var y=new r(g);y.onMove=function(e,n){s+e+u18&&(c=u+e,d.style.width=c+"px"),o+n+a18&&(h=a+n,d.style.height=h+"px")},y.onStop=function(){u=c,a=h}},o=0,u=0,a=0,f=!1,l=!1,c=null,h=this,p=e.offsetWidth,d=e.offsetHeight,v=function(e,t){if(e<-0.5||e>.5)throw"Invalid value: "+t+" must be between 0 and 1";return!0},m=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)},g=function(e){return b.getElementsByClassName(e)[0]},y=function(e){return document.createElement(e)},b=y("div");b.className="photobooth",b.innerHTML='
Sorry, Photobooth.js is not supported by your browser
Please give Photobooth permission to use your Webcam. Try again
';var w=y("canvas"),E=w.getContext("2d"),S=b.getElementsByTagName("canvas")[0],x=S.getContext("2d"),T=y("video");T.autoplay=!0;var N=g("noWebcam");N.getElementsByTagName("span")[0].onclick=function(){M()},new i(g("hue"),function(e){o=e}),new i(g("saturation"),function(e){u=e}),new i(g("brightness"),function(e){a=e});var C=new s(b,p,d),k=g("crop");k.onclick=function(){C.toggle(),k.className==="crop"?k.className="crop selected":k.className="crop"};var L=g("blind");g("trigger").onclick=function(){L.className="blind",L.style.opacity=1,setTimeout(function(){L.className="blind anim",L.style.opacity=0},50);var e={};C.isActive()?e=C.getData():f?e={x:(p-T.videoWidth)/2,y:(d-T.videoHeight)/2,width:T.videoWidth,height:T.videoHeight}:e={x:0,y:0,width:p,height:d};var t=y("canvas");t.width=e.width,t.height=e.height;if(f)t.getContext("2d").drawImage(T,Math.max(0,e.x-(p-T.videoWidth)/2),Math.max(e.y-(d-T.videoHeight)/2),e.width,e.height,0,0,e.width,e.height);else{var n=x.getImageData(e.x,e.y,e.width,e.height);t.getContext("2d").putImageData(n,0,0)}h.onImage(t.toDataURL())};var A=function(e){c=e;try{T.src=(window.URL||window.webkitURL).createObjectURL(c),m(H)}catch(t){T.mozSrcObject=c,h.forceHSB===!1?(f=!0,b.appendChild(T),b.getElementsByTagName("ul")[0].className="noHSB"):T.addEventListener("canplay",function(){m(H)},!1),T.play()}},O=function(e){N.style.display="block"},M=function(){N.style.display="none",t.call(navigator,{video:!0},A,O)},_=function(e,t,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?e+(t-e)*6*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e},D=function(e){return e>1?e-1:e<0?1+e:e},P=function(e){return e>1?1:e<0?0:e},H=function(){try{E.drawImage(T,0,0,p,d)}catch(e){}var t=E.getImageData(0,0,p,d),n=t.data;for(var r=0;r.5?b/(2-c-h):b/(c+h),c===i&&(v=((s-f)/b+(s 0) { for (var i = 0; i < children.length; i++) { var child = children[i]; diff --git a/src/octoprint/plugins/svgtogcode/static/js/render_fills.js b/src/octoprint/plugins/svgtogcode/static/js/render_fills.js new file mode 100644 index 00000000..60b55bd6 --- /dev/null +++ b/src/octoprint/plugins/svgtogcode/static/js/render_fills.js @@ -0,0 +1,164 @@ +// render_fills.js - a snapsvg.io plugin to render the infill of svg files into a bitmap. +// Copyright (C) 2015 Teja Philipp +// +// based on work by http://davidwalsh.name/convert-canvas-image +// and http://getcontext.net/read/svg-images-on-a-html5-canvas +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + + +Snap.plugin(function (Snap, Element, Paper, global) { + + + + /** + * @param {elem} elem start point + * + * @returns {path} + */ + + Element.prototype.removeUnfilled = function(fillPaths){ + var elem = this; + var selection = []; + var children = elem.children(); + + + if (children.length > 0) { + var goRecursive = (elem.type !== "defs" && // ignore these tags + elem.type !== "clipPath" && + elem.type !== "metadata" && + elem.type !== "rdf:rdf" && + elem.type !== "cc:work" && + elem.type !== "sodipodi:namedview"); + + if(goRecursive) { + for (var i = 0; i < children.length; i++) { + var child = children[i]; + selection = selection.concat(child.removeUnfilled(fillPaths)); + } + } + } else { + if(elem.type === 'image'){ + selection.push(elem); + } else { + if(fillPaths && elem.is_filled()){ + selection.push(elem); + } else { + elem.remove(); + } + } + } + return selection; + }; + + Element.prototype.is_filled = function(){ + var elem = this; + + // TODO text support + // TODO opacity support + if (elem.type !== "circle" && + elem.type !== "rect" && + elem.type !== "ellipse" && + elem.type !== "line" && + elem.type !== "polygon" && + elem.type !== "polyline" && + elem.type !== "path" ){ + + return false; + } + + var fill = elem.attr('fill'); + var opacity = elem.attr('fill-opacity'); + + if(fill !== 'none'){ + if(opacity === null || opacity > 0){ + return true; + } + } + return false; + }; + + Element.prototype.embedImage = function(callback){ + var elem = this; + if(elem.type !== 'image') return; + + var url = elem.attr('href'); + var image = new Image(); + + image.onload = function () { + var canvas = document.createElement('canvas'); + canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size + canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size + + canvas.getContext('2d').drawImage(this, 0, 0); + var dataUrl = canvas.toDataURL('image/png'); + elem.attr('href', dataUrl); + canvas.remove(); + if(typeof callback === 'function'){ + callback(elem.attr('id')); + console.log('embedded img'); + } + }; + + image.src = url; + + }; + + Element.prototype.renderPNG = function (wMM, hMM, pxPerMM, callback) { + var elem = this; + + // get svg as dataUrl + var svgStr = elem.outerSVG(); + var svgDataUri = 'data:image/svg+xml;base64,' + window.btoa(svgStr); + var source = new Image(); + source.src = svgDataUri; + + // init render canvas and attach to page + var renderCanvas = document.createElement('canvas'); + renderCanvas.id = "renderCanvas"; + renderCanvas.width = wMM * pxPerMM; + renderCanvas.height = hMM * pxPerMM; + document.getElementsByTagName('body')[0].appendChild(renderCanvas); + var renderCanvasContext = renderCanvas.getContext('2d'); + + // render SVG image to the canvas once it loads. + source.onload = function () { + renderCanvasContext.drawImage(source, 0, 0, renderCanvas.width, renderCanvas.height); + + // place fill bitmap into svg + var fillBitmap = renderCanvas.toDataURL("image/png"); + if(typeof callback === 'function'){ + callback(fillBitmap); + } + renderCanvas.remove(); + }; + + // catch browsers without native svg support + source.onerror = function() { + console.error("Can't export! Maybe your browser doesn't support native SVG. Sorry."); + }; + }; + + +}); + + + + + + + + + diff --git a/src/octoprint/plugins/svgtogcode/static/js/working_area.js b/src/octoprint/plugins/svgtogcode/static/js/working_area.js index b4888f54..3c0d93da 100644 --- a/src/octoprint/plugins/svgtogcode/static/js/working_area.js +++ b/src/octoprint/plugins/svgtogcode/static/js/working_area.js @@ -107,6 +107,8 @@ $(function(){ if(self.state.isOperational() && !self.state.isPrinting()){ var x = self.px2mm(event.offsetX); var y = self.px2mm(event.toElement.ownerSVGElement.offsetHeight - event.offsetY); // hopefully this works across browsers + x = Math.min(x, self.workingAreaWidthMM()); + y = Math.min(y, self.workingAreaHeightMM()); $.ajax({ url: API_BASEURL + "printer/printhead", type: "POST", @@ -310,13 +312,9 @@ $(function(){ var bbox = svg.getBBox(); var tx = self.px2mm(bbox.x * globalScale); var ty = self.workingAreaHeightMM() - self.px2mm(bbox.y2 * globalScale); -// var tx = self.px2mm(svg.data('tx')*globalScale).toFixed(1); -// var ty = -self.px2mm(svg.data('ty')*globalScale).toFixed(1); -// var rot = svg.data('angle') || 0; var startIdx = transform.local.indexOf('r') + 1; var endIdx = transform.local.indexOf(',', startIdx); var rot = parseFloat(transform.local.substring(startIdx, endIdx)) || 0; -// if(!rot) rot = 0; // avoid NaN var horizontal = self.px2mm((bbox.x2 - bbox.x) * globalScale); var vertical = self.px2mm((bbox.y2 - bbox.y) * globalScale); var id = svg.attr('id'); @@ -392,7 +390,7 @@ $(function(){ }); }; - + self.placeIMG = function (file) { var url = self._getIMGserveUrl(file); var img = new Image(); @@ -412,7 +410,7 @@ $(function(){ newImg.attr({id: previewId, filter: 'url(#grayscale_filter)', 'data-serveurl': url}); snap.select("#userContent").append(newImg); newImg.transformable(); - newImg.ftDisableRotate(); + //newImg.ftDisableRotate(); newImg.ftRegisterCallback(self.svgTransformUpdate); file.id = id; file.previewId = previewId; @@ -623,19 +621,36 @@ $(function(){ self.check_sizes_and_placements(); }; - self.getCompositionSVG = function(){ + self.getCompositionSVG = function(fillAreas, callback){ self.abortFreeTransforms(); - var tmpsvg = snap.select("#userContent").innerSVG(); // get working area - if(tmpsvg !== ''){ + var wMM = self.workingAreaWidthMM(); + var hMM = self.workingAreaHeightMM(); + var wPT = wMM * 90 / 25.4; + var hPT = hMM * 90 / 25.4; + var compSvg = Snap(wPT, hPT); + compSvg.attr('id', 'compSvg'); + + var userContent = snap.select("#userContent").clone(); + compSvg.append(userContent); + + self.renderInfill(compSvg, fillAreas, wMM, hMM, 10, function(svgWithRenderedInfill){ + callback( self._wrapInSvgAndScale(svgWithRenderedInfill)); + $('#compSvg').remove(); + }); + }; + + self._wrapInSvgAndScale = function(content){ + var svgStr = content.innerSVG(); + if(svgStr !== ''){ var dpiFactor = self.svgDPI()/25.4; // convert mm to pix 90dpi for inkscape, 72 for illustrator var w = dpiFactor * self.workingAreaWidthMM(); var h = dpiFactor * self.workingAreaHeightMM(); // TODO: look for better solution to solve this Firefox bug problem - tmpsvg = tmpsvg.replace("(\\\"","("); - tmpsvg = tmpsvg.replace("\\\")",")"); + svgStr = svgStr.replace("(\\\"","("); + svgStr = svgStr.replace("\\\")",")"); - var svg = ''+ tmpsvg +''; + var svg = ''+ svgStr +''; return svg; } else { return; @@ -667,6 +682,19 @@ $(function(){ return gcodeFiles; }, self); + self.hasFilledVectors = function(){ + var el = snap.selectAll('#userContent *'); + for (var i = 0; i < el.length; i++) { + var e = el[i]; + var fill = e.attr('fill'); + var op = e.attr('fill-opacity'); + if(fill !== 'none' && op > 0){ + return true; + } + + } + return false; + }; self.draw_gcode = function(points, intensity, target){ var stroke_color = intensity === 0 ? '#BBBBBB' : '#FF0000'; @@ -680,11 +708,6 @@ $(function(){ }; self.draw_gcode_img_placeholder = function(x,y,w,h,url, target){ - var i = snap.rect(x,y,w,h).attr({ - stroke: '#AA0000', - 'stroke-width': 1 - }); - snap.select(target).append(i); if(url !== ""){ var p = snap.image(url,x,y,w,h).attr({ transform: 'matrix(1,0,0,-1,0,'+ String(h) +')', @@ -698,7 +721,7 @@ $(function(){ self.clear_gcode = function(){ snap.select('#gCodePreview').clear(); }; - + self.onStartup = function(){ self.state.workingArea = self; self.files.workingArea = self; @@ -731,6 +754,79 @@ $(function(){ } }); }; + + self._embedAllImages = function(svg, callback){ + + var allImages = svg.selectAll('image'); + var linkedImages = allImages.items.filter(function(i){ return !i.attr('href').startsWith('data:') }); + if(linkedImages.length > 0){ + var callbackCounter = linkedImages.length; + for (var i = 0; i < linkedImages.length; i++) { + var img = linkedImages[i]; + img.embedImage(function(){ + callbackCounter--; + if(callbackCounter === 0 && typeof callback === 'function'){ + callback(); + } + }); + } + } else { + // callback if nothing to embed + if(typeof callback === 'function'){ + callback(); + } + } + } + + // render the infill and inject it as an image into the svg + self.renderInfill = function (svg, fillAreas, wMM, hMM, pxPerMM, callback) { + var wPT = wMM * 90 / 25.4; + var hPT = hMM * 90 / 25.4; + var tmpSvg = Snap(wPT, hPT).attr('id', 'tmpSvg'); + // get only filled items and embed the images + var userContent = svg.clone(); + tmpSvg.append(userContent); + self._embedAllImages(tmpSvg, function(){ + var fillings = userContent.removeUnfilled(fillAreas); + for (var i = 0; i < fillings.length; i++) { + var item = fillings[i]; + + if (item.type === 'image') { + // remove filter effects on images for proper rendering + var style = item.attr('style'); + if (style !== null) { + var strippedFilters = style.replace(/filter.+?;/, ''); + item.attr('style', strippedFilters); + } + } else { + // remove stroke from other elements + //item.attr('fill', '#ff0000'); + item.attr('stroke', 'none'); + } + } + + var cb = function(result) { + if(fillings.length > 0){ + // replace all images with the fill rendering + svg.selectAll('image').remove(); + var waBB = snap.select('#coordGrid').getBBox(); + var fillImage = snap.image(result, 0, 0, waBB.w, waBB.h); + fillImage.attr('id', 'fillRendering'); + svg.append(fillImage); + } + if (typeof callback === 'function') { + callback(svg); + } + self._cleanup_render_mess(); + }; + + tmpSvg.renderPNG(wMM, hMM, pxPerMM, cb); + }); + }; + + self._cleanup_render_mess = function(){ + $('#tmpSvg').remove(); + }; self.onBeforeBinding = function(){ self.files.workingArea = self; diff --git a/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2 b/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2 index ee46cc96..0eba6848 100644 --- a/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2 +++ b/src/octoprint/plugins/svgtogcode/templates/svgtogcode.jinja2 @@ -38,6 +38,12 @@ The effect in general is dependent from the material and its color and surface. + +
+ +
@@ -176,7 +182,7 @@
- + {{ _('Convert') }} \ No newline at end of file diff --git a/src/octoprint/printer/__init__.py b/src/octoprint/printer/__init__.py index b518cb20..2a4446db 100644 --- a/src/octoprint/printer/__init__.py +++ b/src/octoprint/printer/__init__.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import re -import octoprint.util.comm_acc as comm +import octoprint.util.comm_acc2 as comm import octoprint.util as util from octoprint.settings import settings diff --git a/src/octoprint/printer/standard.py b/src/octoprint/printer/standard.py index b0827966..5fee8558 100644 --- a/src/octoprint/printer/standard.py +++ b/src/octoprint/printer/standard.py @@ -22,7 +22,7 @@ from octoprint.plugin import plugin_manager, ProgressPlugin from octoprint.printer import PrinterInterface, PrinterCallback, UnknownScript from octoprint.printer.estimation import TimeEstimationHelper from octoprint.settings import settings -from octoprint.util import comm_acc as comm +from octoprint.util import comm_acc2 as comm from octoprint.util import InvariantContainer diff --git a/src/octoprint/static/js/app/viewmodels/control.js b/src/octoprint/static/js/app/viewmodels/control.js index 71ecc7b4..cf3b18bf 100644 --- a/src/octoprint/static/js/app/viewmodels/control.js +++ b/src/octoprint/static/js/app/viewmodels/control.js @@ -108,11 +108,11 @@ $(function() { }); } }; - + self.rerenderControls = function () { var allControls = self.controlsFromServer.concat(self.additionalControls); - self.controls(self._processControls(allControls)) + self.controls(self._processControls(allControls)); }; self.requestData = function () { diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 45fe0290..b4f71d85 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -100,7 +100,7 @@ $(function() { }); } }; - + self.fromCurrentData = function(data) { self._processStateData(data.state); }; @@ -138,7 +138,7 @@ $(function() { } }); }; - + self.fromResponse = function(response, filenameToFocus, locationToFocus) { var files = response.files; _.each(files, function(element, index, list) { @@ -267,7 +267,7 @@ $(function() { return "files_template_dummy"; } }; - + self.getEntryId = function(data) { return "gcode_file_" + md5(data["origin"] + ":" + data["name"]); }; @@ -344,7 +344,7 @@ $(function() { } return output; }; - + self.performSearch = function(e) { if (e !== undefined) { e.preventDefault(); @@ -375,7 +375,7 @@ $(function() { self.enableSVGConversion = function (data) { return self.loginState.isUser() && !(self.isPrinting() || self.isPaused()); }; - + self.onStartup = function() { $(".accordion-toggle[data-target='#files']").click(function() { var files = $("#files"); @@ -603,6 +603,90 @@ $(function() { }, 1000); }); + $('#take_photo_dialog').on('hide', function () { + $('#photo_preview').data("photobooth").destroy(); + }); + + + $('#take_photo_dialog').on('shown', function () { + $('#photo_preview').photobooth(); + var w = $('#photo_preview').parent().width()*0.98; + var h = w*3.0/4.0; + $('#photo_preview').height(h); + $('#photo_preview').width(w); + $('#photo_preview').data('photobooth').resize(w, h); + }); + + $('#photo_preview').on("image", function (event, dataUrl) { + var photoBlob = self.dataUriToBlob(dataUrl); + var t = new Date(); + var yyyy = t.getFullYear().toString(); + var mm = (t.getMonth()+1).toString(); // getMonth() is zero-based + var dd = t.getDate().toString(); + var hh = t.getHours().toString(); + var m = t.getMinutes().toString(); + var date = yyyy + (mm[1]?mm:"0"+mm[0]) + (dd[1]?dd:"0"+dd[0]) + '_' + (hh[1]?hh:"0"+hh[0])+(m[1]?m:"0"+m[0]); // padding + + var filename = "Photo_" + date + ".png"; + var data = new FormData(); + data.append('file', photoBlob, filename); + + jQuery.ajax({ + url: API_BASEURL + "files/local", + data: data, + cache: false, + contentType: false, + processData: false, + type: 'POST', + success: function(data, resp){ + gcode_upload_done(resp, {result: data}); + $('#take_photo_dialog').modal("hide"); + }, + fail: gcode_upload_fail, + progressall: gcode_upload_progress + }); + }); + + self.takePhoto = function () { + $('#take_photo_dialog').modal("show"); + }; + + self.hasCamera = function () { + var fGetUserMedia = ( + navigator.getUserMedia || + navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || + navigator.oGetUserMedia || + navigator.msieGetUserMedia || + false + ); + return !!fGetUserMedia; + }; + + self.dataUriToBlob = function(dataURI) { + // serialize the base64/URLEncoded data + var byteString; + if (dataURI.split(',')[0].indexOf('base64') >= 0) { + byteString = atob(dataURI.split(',')[1]); + } + else { + byteString = unescape(dataURI.split(',')[1]); + } + + // parse the mime type + var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + // construct a Blob of the image data + var array = []; + for (var i = 0; i < byteString.length; i++) { + array.push(byteString.charCodeAt(i)); + } + return new Blob( + [new Uint8Array(array)], + {type: mimeString} + ); + }; + self.requestData(); }; diff --git a/src/octoprint/static/js/app/viewmodels/printerstate.js b/src/octoprint/static/js/app/viewmodels/printerstate.js index 1ceacb01..b6b8f6d5 100644 --- a/src/octoprint/static/js/app/viewmodels/printerstate.js +++ b/src/octoprint/static/js/app/viewmodels/printerstate.js @@ -34,6 +34,10 @@ $(function() { self.currentHeight = ko.observable(undefined); self.currentPos = ko.observable(undefined); + self.intensityOverride = ko.observable(100); + self.feedrateOverride = ko.observable(100); + self.intensityOverride.extend({ rateLimit: 500 }); + self.feedrateOverride.extend({ rateLimit: 500 }); self.TITLE_PRINT_BUTTON_PAUSED = gettext("Restarts the print job from the beginning"); self.TITLE_PRINT_BUTTON_UNPAUSED = gettext("Starts the print job"); @@ -281,6 +285,62 @@ $(function() { self.onEventRealTimeState = function(payload){ self.currentPos({x: payload.wx, y: payload.wy}); }; + + self.intensityOverride.subscribe(function(factor){ + self._overrideCommand("/intensity "+factor); + }); + self.feedrateOverride.subscribe(function(factor){ + self._overrideCommand("/feedrate "+factor); + }); + + self._overrideCommand = function(command, callback) { + $.ajax({ + url: API_BASEURL + "printer/command", + type: "POST", + dataType: "json", + contentType: "application/json; charset=UTF-8", + data: JSON.stringify({command: command}), + success: function(response) { + if (callback != undefined) { + callback(); + } + } + }); + }; + + self._configureOverrideSliders = function() { + self.intensityOverrideSlider = $("#intensity_override_slider").slider({ + step: 1, + min: 10, + max: 200, + value: 100, +// tooltip: 'hide' + }).on("slideStop", function(ev){ + self.intensityOverride(ev.value); + }); + + self.feedrateOverrideSlider = $("#feedrate_override_slider").slider({ + step: 1, + min: 10, + max: 200, + value: 100, +// tooltip: 'hide' + }).on("slideStop", function(ev){ + self.feedrateOverride(ev.value); + }); + + }; + + self.onEventPrintDone = function(){ + self.feedrateOverrideSlider.slider('setValue', 100); + self.intensityOverrideSlider.slider('setValue', 100); + self.intensityOverride(100); + self.feedrateOverride(100); + }; + + self.onStartup = function() { + self._configureOverrideSliders(); + }; } OCTOPRINT_VIEWMODELS.push([ diff --git a/src/octoprint/templates/dialogs/take_photo.jinja2 b/src/octoprint/templates/dialogs/take_photo.jinja2 new file mode 100644 index 00000000..f91b94a5 --- /dev/null +++ b/src/octoprint/templates/dialogs/take_photo.jinja2 @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index cae2ac47..d94d0733 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -15,27 +15,6 @@ {% include 'initscript.jinja2' %} -
- {% include 'dialogs/confirmation.jinja2' %} diff --git a/src/octoprint/templates/mrbeam_index.jinja2 b/src/octoprint/templates/mrbeam_index.jinja2 index 5d454b5b..86b64cdf 100644 --- a/src/octoprint/templates/mrbeam_index.jinja2 +++ b/src/octoprint/templates/mrbeam_index.jinja2 @@ -155,8 +155,20 @@ {{ _('Timelapse') }}:
--> {{ _('Approx. Total Job Time') }}:
-
-
 {{ _('Processed') }} :
+
+ +
+
 {{ _('Processed') }} :
+
+
+ + 100% Intensity +
+
+ + 100% Feedrate +
+
@@ -288,13 +300,11 @@ - + - + + @@ -494,11 +507,16 @@
- + {{ _('Upload') }} + + + {{ _('Photo') }} + +
@@ -715,6 +733,7 @@ {#% include 'dialogs/settings.jinja2' %#} {% include 'dialogs/slicing.jinja2' %} {% include 'dialogs/usersettings.jinja2' %} + {% include 'dialogs/take_photo.jinja2' %} diff --git a/src/octoprint/util/comm_acc.py b/src/octoprint/util/comm_acc.py index 83d6b82f..8dacbdd5 100644 --- a/src/octoprint/util/comm_acc.py +++ b/src/octoprint/util/comm_acc.py @@ -1888,22 +1888,34 @@ class MachineCom(object): self._doSendWithoutChecksum(commandToSend) def _doSendWithoutChecksum(self, cmd): - self._log("Send: %s" % cmd) - self.acc_line_lengths.append(len(cmd)+1) # Track number of characters in grbl serial read buffer - try: - self._serial.write(cmd + '\n') - except serial.SerialTimeoutException: - self._log("Serial timeout while writing to serial port, trying again.") + if cmd == "?": + try: + self._serial.write(cmd) + except serial.SerialTimeoutException: + self._log("Serial timeout while writing to serial port, trying again.") + try: + self._serial.write(cmd) + except: + self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) + else: + self._log("Send: %s" % cmd) + self.acc_line_lengths.append(len(cmd)+1) # Track number of characters in grbl serial read buffer try: self._serial.write(cmd + '\n') + except serial.SerialTimeoutException: + self._log("Serial timeout while writing to serial port, trying again.") + try: + self._serial.write(cmd + '\n') + except: + self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) except: self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) self._errorValue = get_exception_string() self.close(True) - except: - self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) - self._errorValue = get_exception_string() - self.close(True) ##~~ command handlers def _gcode_H_sent(self, cmd, cmd_type=None): diff --git a/src/octoprint/util/comm_acc2.py b/src/octoprint/util/comm_acc2.py new file mode 100644 index 00000000..6a9f7bfd --- /dev/null +++ b/src/octoprint/util/comm_acc2.py @@ -0,0 +1,1265 @@ +# coding=utf-8 +from __future__ import absolute_import +__author__ = "Florian Becker based on work by Gina Häußge and David Braam" +__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" +__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License" + +import os +import threading +import logging +import glob +import time +import serial +import re +import Queue + +from yaml import load as yamlload +from yaml import dump as yamldump +from subprocess import call as subprocesscall + +import octoprint.plugin + +from octoprint.settings import settings, default_settings +from octoprint.events import eventManager, Events +from octoprint.filemanager.destinations import FileDestinations +from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii + +### MachineCom ######################################################################################################### +class MachineCom(object): + STATE_NONE = 0 + STATE_OPEN_SERIAL = 1 + STATE_DETECT_SERIAL = 2 + STATE_CONNECTING = 3 + STATE_OPERATIONAL = 4 + STATE_PRINTING = 5 + STATE_PAUSED = 6 + STATE_CLOSED = 7 + STATE_ERROR = 8 + STATE_CLOSED_WITH_ERROR = 9 + STATE_LOCKED = 10 + STATE_HOMING = 11 + STATE_FLASHING = 12 + + def __init__(self, port=None, baudrate=None, callbackObject=None, printerProfileManager=None): + self._logger = logging.getLogger(__name__) + self._serialLogger = logging.getLogger("SERIAL") + + if port is None: + port = settings().get(["serial", "port"]) + elif isinstance(port, list): + port = port[0] + if baudrate is None: + settingsBaudrate = settings().getInt(["serial", "baudrate"]) + if settingsBaudrate is None: + baudrate = 0 + else: + baudrate = settingsBaudrate + if callbackObject is None: + callbackObject = MachineComPrintCallback() + + self._port = port + self._baudrate = baudrate + self._callback = callbackObject + self._printerProfileManager = printerProfileManager + + self.RX_BUFFER_SIZE = 127 + + self._state = self.STATE_NONE + self._grbl_state = None + self._errorValue = "Unknown Error" + self._serial = None + self._currentFile = None + self._status_timer = None + self._acc_line_buffer = [] + self._cmd = None + self._pauseWaitStartTime = None + self._pauseWaitTimeLost = 0.0 + self._commandQueue = Queue.Queue() + self._send_event = CountedEvent(max=50) + self._finished_currentFile = False + self._pause_delay_time = 0 + self._feedrate_factor = 1 + self._actual_feedrate = None + self._intensity_factor = 1 + self._actual_intensity = None + self._feedrate_dict = {} + self._intensity_dict = {} + + # regular expressions + self._regex_command = re.compile("^\s*\$?([GM]\d+|[TH])") + self._regex_feedrate = re.compile("F\d+", re.IGNORECASE) + self._regex_intensity = re.compile("S\d+", re.IGNORECASE) + + self._real_time_commands={'poll_status':False, + 'feed_hold':False, + 'cycle_start':False, + 'soft_reset':False} + + # metric stuff + #self._metric_chars = 0 + #self._metric_time = None + #self._metric_active = True + #self._metric_thread = threading.Thread(target=self._metric_loop, name="comm._metric_thread") + #self._metric_thread.daemon = True + #self._metric_thread.start() + + # hooks + self._pluginManager = octoprint.plugin.plugin_manager() + self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory") + + # monitoring thread + self._monitoring_active = True + self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitoring_thread") + self.monitoring_thread.daemon = True + self.monitoring_thread.start() + + # sending thread + self._sending_active = True + self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread") + self.sending_thread.daemon = True + + def _monitor_loop(self): + #Open the serial port. + if not self._openSerial(): + return + + self._log("Connected to: %s, starting monitor" % self._serial) + self._changeState(self.STATE_CONNECTING) + self._timeout = get_new_timeout("communication") + + while self._monitoring_active: + try: + line = self._readline() + if line is None: + break + if line.strip() is not "": + self._timeout = get_new_timeout("communication") + if line.startswith('<'): # status report + self._handle_status_report(line) + elif line.startswith('ok'): # ok message :) + self._handle_ok_message() + elif line.startswith('err'): # error message + self._handle_error_message(line) + elif line.startswith('ALA'): # ALARM message + self._handle_alarm_message(line) + elif line.startswith('['): # feedback message + self._handle_feedback_message(line) + elif line.startswith('Grb'): # Grbl startup message + self._handle_startup_message() + except: + self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam") + errorMsg = "See octoprint.log for details" + self._log(errorMsg) + self._errorValue = errorMsg + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._log("Connection closed, closing down monitor") + + def _send_loop(self): + while self._sending_active: + try: + self._process_rt_commands() + if self.isPrinting() and self._commandQueue.empty(): + cmd = self._getNext() + if cmd is not None: + self.sendCommand(cmd) + self._callback.on_comm_progress() + elif len(self._acc_line_buffer) == 0: + self._set_print_finished() + + self._sendCommand() + self._send_event.wait(1) + self._send_event.clear() + except: + self._logger.exception("Something crashed inside the sending loop, please report this to Mr. Beam") + errorMsg = "See octoprint.log for details" + self._log(errorMsg) + self._errorValue = errorMsg + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + + def _metric_loop(self): + self._metricf = open('metric.tmp','w') + self._metricf.write("1 sec interval") + while self._metric_active: + time.sleep(1) + if self._metric_time is not None: + t = time.time() + #s = "Metric: %f [chars/sec]" % (self._metric_chars / (t - self._metric_time)) + s = "%.2f" % (self._metric_chars / (t - self._metric_time)) + self._metric_time = None + self._metric_chars = 0 + self._metricf.write(s) + #self._log(s) + self._metricf.close() + + def _sendCommand(self, cmd=None): + if cmd is None: + if self._cmd is None and self._commandQueue.empty(): + return + elif self._cmd is None: + self._cmd = self._commandQueue.get() + if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-5: + self._cmd, _, _ = self._process_command_phase("sending", self._cmd) + self._log("Send: %s" % self._cmd) + self._acc_line_buffer.append(self._cmd + '\n') + try: + self._serial.write(self._cmd + '\n') + self._process_command_phase("sent", self._cmd) + self._cmd = None + self._send_event.set() + except serial.SerialException: + self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) + else: + self._log("Send: %s" % cmd) + try: + self._cmd, _, _ = self._process_command_phase("sending", self._cmd) + self._serial.write(cmd) + self._process_command_phase("sent", cmd) + except serial.SerialException: + self._log("Unexpected error while writing serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) + + def _process_rt_commands(self): + if self._real_time_commands['poll_status']: + self._sendCommand('?') + self._real_time_commands['poll_status']=False + elif self._real_time_commands['feed_hold']: + self._sendCommand('!') + self._real_time_commands['feed_hold']=False + elif self._real_time_commands['cycle_start']: + self._sendCommand('~') + self._real_time_commands['cycle_start']=False + elif self._real_time_commands['soft_reset']: + self._sendCommand(b'\x18') + self._real_time_commands['soft_reset']=False + + def _openSerial(self): + def default(_, port, baudrate, read_timeout): + if port is None or port == 'AUTO': + # no known port, try auto detection + self._changeState(self.STATE_DETECT_SERIAL) + ser = self._detectPort(True) + if ser is None: + self._errorValue = 'Failed to autodetect serial port, please set it manually.' + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._log("Failed to autodetect serial port, please set it manually.") + return None + port = ser.port + + # connect to regular serial port + self._log("Connecting to: %s" % port) + if baudrate == 0: + baudrates = baudrateList() + ser = serial.Serial(str(port), 115200 if 115200 in baudrates else baudrates[0], timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD) + else: + ser = serial.Serial(str(port), baudrate, timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD) + ser.close() + ser.parity = serial.PARITY_NONE + ser.open() + return ser + + serial_factories = self._serial_factory_hooks.items() + [("default", default)] + for name, factory in serial_factories: + try: + serial_obj = factory(self, self._port, self._baudrate, settings().getFloat(["serial", "timeout", "connection"])) + except (OSError, serial.SerialException): + exception_string = get_exception_string() + self._errorValue = "Connection error, see Terminal tab" + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name)) + if "failed to set custom baud rate" in exception_string.lower(): + self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian") + return False + if serial_obj is not None: + # first hook to succeed wins, but any can pass on to the next + self._log(repr(self._serial)) + self._changeState(self.STATE_OPEN_SERIAL) + self._serial = serial_obj + return True + return False + + def _readline(self): + if self._serial is None: + return None + try: + ret = self._serial.readline() + self._send_event.set() + if('ok' in ret or 'error' in ret): + if(len(self._acc_line_buffer) > 0): + del self._acc_line_buffer[0] # Delete the commands character count corresponding to the last 'ok' + except serial.SerialException: + self._log("Unexpected error while reading serial port: %s" % (get_exception_string())) + self._errorValue = get_exception_string() + self.close(True) + return None + if ret == '': return '' + try: + self._log("Recv: %s" % sanitize_ascii(ret)) + except ValueError as e: + self._log("WARN: While reading last line: %s" % e) + self._log("Recv: %r" % ret) + return ret + + def _getNext(self): + if self._finished_currentFile is False: + line = self._currentFile.getNext() + if line is None: + self._finished_currentFile = True + return line + else: + return None + + def _set_print_finished(self): + self._callback.on_comm_print_job_done() + self._changeState(self.STATE_OPERATIONAL) + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation(), + "time": self.getPrintTime() + } + eventManager().fire(Events.PRINT_DONE, payload) + self.sendCommand("M5") + self.sendCommand("G0X0Y0") + self.sendCommand("M9") + + def _handle_status_report(self, line): + self._grbl_state = line[1:].split(',')[0] + if self._grbl_state == 'Queue': + if time.time() - self._pause_delay_time > 0.3: + if not self.isPaused(): + self.setPause(True, False) + elif self._grbl_state == 'Run' or self._grbl_state == 'Idle': + if time.time() - self._pause_delay_time > 0.3: + if self.isPaused(): + self.setPause(False, False) + self._update_grbl_pos(line) + #if self._metricf is not None: + # self._metricf.write(line) + + def _handle_ok_message(self): + if self._state == self.STATE_HOMING: + self._changeState(self.STATE_OPERATIONAL) + + def _handle_error_message(self, line): + self._errorValue = line + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + self._changeState(self.STATE_LOCKED) + + def _handle_alarm_message(self, line): + if "Hard/soft limit" in line: + errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle" + self._log(errorMsg) + self._errorValue = errorMsg + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()}) + elif "Abort during cycle" in line: + errorMsg = "Soft-reset detected. Please do a homing cycle" + self._log(errorMsg) + self._errorValue = errorMsg + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + eventManager().fire(Events.SOFT_RESET, {"error": self.getErrorString()}) + elif "Probe fail" in line: + errorMsg = "Probing has failed. Please reset the machine and do a homing cycle" + self._log(errorMsg) + self._errorValue = errorMsg + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + + with self._commandQueue.mutex: + self._commandQueue.queue.clear() + self._acc_line_buffer = [] + self._send_event.clear(completely=True) + self._changeState(self.STATE_LOCKED) + + def _handle_feedback_message(self, line): + if line[1:].startswith('Res'): # [Reset to continue] + pass + elif line[1:].startswith('\'$H'): # ['$H'|'$X' to unlock] + pass + elif line[1:].startswith('Cau'): # [Caution: Unlocked] + pass + elif line[1:].startswith('Ena'): # [Enabled] + pass + elif line[1:].startswith('Dis'): # [Disabled] + pass + + def _handle_startup_message(self): + if self.isOperational(): + errorMsg = "Machine reset." + self._cmd = None + self._acc_line_buffer = [] + self._pauseWaitStartTime = None + self._pauseWaitTimeLost = 0.0 + self._send_event.clear(completely=True) + with self._commandQueue.mutex: + self._commandQueue.queue.clear() + self._log(errorMsg) + self._errorValue = errorMsg + self._changeState(self.STATE_LOCKED) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + else: + self._onConnected(self.STATE_LOCKED) + + def _update_grbl_pos(self, line): + # line example: + # + try: + idx_mx_begin = line.index('MPos:') + 5 + idx_mx_end = line.index('.', idx_mx_begin) + 2 + idx_my_begin = line.index(',', idx_mx_end) + 1 + idx_my_end = line.index('.', idx_my_begin) + 2 + #idx_mz_begin = line.index(',', idx_my_end) + 1 + #idx_mz_end = line.index('.', idx_mz_begin) + 2 + + idx_wx_begin = line.index('WPos:') + 5 + idx_wx_end = line.index('.', idx_wx_begin) + 2 + idx_wy_begin = line.index(',', idx_wx_end) + 1 + idx_wy_end = line.index('.', idx_wy_begin) + 2 + #idx_wz_begin = line.index(',', idx_wy_end) + 1 + #idx_wz_end = line.index('.', idx_wz_begin) + 2 + + #idx_intensity_begin = line.index('S:', idx_wz_end) + 2 + #idx_intensity_end = line.index(',', idx_intensity_begin) + + #idx_laserstate_begin = line.index('laser ', idx_intensity_end) + 6 + #idx_laserstate_end = line.index(':', idx_laserstate_begin) + + #payload = { + #"mx": line[idx_mx_begin:idx_mx_end], + #"my": line[idx_my_begin:idx_my_end], + #"mz": line[idx_mz_begin:idx_mz_end], + #"wx": line[idx_wx_begin:idx_wx_end], + #"wy": line[idx_wy_begin:idx_wy_end], + #"wz": line[idx_wz_begin:idx_wz_end], + #"laser": line[idx_laserstate_begin:idx_laserstate_end], + #"intensity": line[idx_intensity_begin:idx_intensity_end] + #} + mx = float(line[idx_mx_begin:idx_mx_end]) + my = float(line[idx_my_begin:idx_my_end]) + wx = float(line[idx_wx_begin:idx_wx_end]) + wy = float(line[idx_wy_begin:idx_wy_end]) + self._callback.on_comm_pos_update([mx, my, 0], [wx, wy, 0]) + #eventManager().fire(Events.RT_STATE, payload) + except ValueError: + pass + + def _process_command_phase(self, phase, command, command_type=None, gcode=None): + if phase not in ("queuing", "queued", "sending", "sent"): + return command, command_type, gcode + + if gcode is None: + gcode = self._gcode_command_for_cmd(command) + + # if it's a gcode command send it through the specific handler if it exists + if gcode is not None: + gcodeHandler = "_gcode_" + gcode + "_" + phase + if hasattr(self, gcodeHandler): + handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type) + command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result) + + # finally return whatever we resulted on + return command, command_type, gcode + + def _gcode_command_for_cmd(self, cmd): + """ + Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0"). + + Arguments: + cmd (str): The command to try to parse. + + Returns: + str or None: The GCODE command identifier if it could be parsed, or None if not. + """ + if not cmd: + return None + + if cmd == '!': return 'Hold' + if cmd == '~': return 'Resume' + + gcode = self._regex_command.search(cmd) + if not gcode: + return None + + return gcode.group(1) + + # internal state management + def _changeState(self, newState): + if self._state == newState: + return + + if newState == self.STATE_PRINTING: + if self._status_timer is not None: + self._status_timer.cancel() + self._status_timer = RepeatedTimer(1, self._poll_status) + self._status_timer.start() + elif newState == self.STATE_OPERATIONAL: + if self._status_timer is not None: + self._status_timer.cancel() + self._status_timer = RepeatedTimer(2, self._poll_status) + self._status_timer.start() + elif newState == self.STATE_PAUSED: + if self._status_timer is not None: + self._status_timer.cancel() + self._status_timer = RepeatedTimer(0.2, self._poll_status) + self._status_timer.start() + + if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR: + if self._currentFile is not None: + self._currentFile.close() + self._log("entered state closed / closed with error. reseting character counter.") + self.acc_line_lengths = [] + + oldState = self.getStateString() + self._state = newState + self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString())) + self._callback.on_comm_state_change(newState) + + def _onConnected(self, nextState): + self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"]) + + if(nextState is None): + self._changeState(self.STATE_LOCKED) + else: + self._changeState(nextState) + + if not self.sending_thread.isAlive(): + self.sending_thread.start() + + payload = dict(port=self._port, baudrate=self._baudrate) + eventManager().fire(Events.CONNECTED, payload) + + def _detectPort(self, close): + self._log("Serial port list: %s" % (str(serialList()))) + for p in serialList(): + try: + self._log("Connecting to: %s" % (p)) + serial_obj = serial.Serial(p) + if close: + serial_obj.close() + return serial_obj + except (OSError, serial.SerialException) as e: + self._log("Error while connecting to %s: %s" % (p, str(e))) + return None + + def _poll_status(self): + if self.isOperational(): + self._real_time_commands['poll_status']=True + self._send_event.set() + + def _soft_reset(self): + if self.isOperational(): + self._real_time_commands['soft_reset']=True + self._send_event.set() + + def _log(self, message): + self._callback.on_comm_log(message) + self._serialLogger.debug(message) + + def _compareGrblVersion(self, versionDict): + cwd = os.path.dirname(__file__) + with open(cwd + "/../grbl/grblVersionRequirement.yml", 'r') as infile: + grblReqDict = yamlload(infile) + requiredGrblVer = str(grblReqDict['grbl']) + '_' + str(grblReqDict['git']) + if grblReqDict['dirty'] is True: + requiredGrblVer += '-dirty' + actualGrblVer = str(versionDict['grbl']) + '_' + str(versionDict['git']) + if versionDict['dirty'] is not(None): + actualGrblVer += '-dirty' + # compare actual and required grbl version + self._requiredGrblVer = requiredGrblVer + self._actualGrblVer = actualGrblVer + print repr(requiredGrblVer) + print repr(actualGrblVer) + if requiredGrblVer != actualGrblVer: + self._log("unsupported grbl version detected...") + self._log("required: " + requiredGrblVer) + self._log("detected: " + actualGrblVer) + return False + else: + return True + + def _flashGrbl(self): + self._changeState(self.STATE_FLASHING) + self._serial.close() + cwd = os.path.dirname(__file__) + pathToGrblHex = cwd + "/../grbl/grbl.hex" + + # TODO check if avrdude is installed. + # TODO log in logfile as well, not only to the serial monitor (use self._logger.info()... ) + params = ["avrdude", "-patmega328p", "-carduino", "-b" + str(self._baudrate), "-P" + str(self._port), "-D", "-Uflash:w:" + pathToGrblHex] + rc = subprocesscall(params) + + if rc is False: + self._log("successfully flashed new grbl version") + self._openSerial() + self._changeState(self.STATE_CONNECTING) + else: + self._log("error during flashing of new grbl version") + self._errorValue = "avrdude returncode: %s" % rc + self._changeState(self.STATE_CLOSED_WITH_ERROR) + + @staticmethod + def _writeGrblVersionToFile(versionDict): + if versionDict['dirty'] == '-dirty': + versionDict['dirty'] = True + versionDict['lastConnect'] = time.time() + versionFile = os.path.join(settings().getBaseFolder("logs"), 'grbl_Version.yml') + with open(versionFile, 'w') as outfile: + outfile.write(yamldump(versionDict, default_flow_style=True)) + + def _handle_command_handler_result(self, command, command_type, gcode, handler_result): + original_tuple = (command, command_type, gcode) + + if handler_result is None: + # handler didn't return anything, we'll just continue + return original_tuple + + if isinstance(handler_result, basestring): + # handler did return just a string, we'll turn that into a 1-tuple now + handler_result = (handler_result,) + elif not isinstance(handler_result, (tuple, list)): + # handler didn't return an expected result format, we'll just ignore it and continue + return original_tuple + + hook_result_length = len(handler_result) + if hook_result_length == 1: + # handler returned just the command + command, = handler_result + elif hook_result_length == 2: + # handler returned command and command_type + command, command_type = handler_result + else: + # handler returned a tuple of an unexpected length + return original_tuple + + gcode = self._gcode_command_for_cmd(command) + return command, command_type, gcode + + def _set_feedrate_override(self, value): + temp = value / 100.0 + if temp > 0: + self._feedrate_factor = temp + self._feedrate_dict = {} + if self._actual_feedrate is not None: + temp = round(self._actual_feedrate * self._feedrate_factor) + # TODO replace with value from printer profile + if temp > 5000: + temp = 5000 + self.sendCommand('F%d' % round(temp)) + + def _set_intensity_override(self, value): + temp = value / 100.0 + if temp >= 0: + self._intensity_factor = temp + self._intensity_dict = {} + if self._actual_intensity is not None: + temp = round(self._actual_intensity * self._intensity_factor) + if temp > 1000: + temp = 1000 + self.sendCommand('S%d' % round(temp)) + + def _replace_feedrate(self, cmd): + if self._feedrate_factor != 1: + obj = self._regex_feedrate.search(cmd) + if obj is not None: + feedrate_cmd = cmd[obj.start():obj.end()] + if feedrate_cmd in self._feedrate_dict: + new_feedrate = self._feedrate_dict[feedrate_cmd] + else: + self._actual_feedrate = int(feedrate_cmd[1:]) + new_feedrate = round(self._actual_feedrate * self._feedrate_factor) + # TODO replace with value from printer profile + if new_feedrate > 5000: + new_feedrate = 5000 + elif new_feedrate < 30: + new_feedrate = 30 + self._feedrate_dict[feedrate_cmd] = new_feedrate + else: + return cmd + return cmd.replace(feedrate_cmd, 'F%d' % round(new_feedrate)) + return cmd + + def _replace_intensity(self, cmd): + if self._intensity_factor != 1: + obj = self._regex_intensity.search(cmd) + if obj is not None: + intensity_cmd = cmd[obj.start():obj.end()] + if intensity_cmd in self._intensity_dict: + new_intensity = self._intensity_dict[intensity_cmd] + else: + self._actual_intensity = int(intensity_cmd[1:]) + new_intensity = round(self._actual_intensity * self._intensity_factor) + if new_intensity > 1000: + new_intensity = 1000 + self._intensity_dict[intensity_cmd] = new_intensity + else: + return cmd + return cmd.replace(intensity_cmd, 'S%d' % round(new_intensity)) + return cmd + + ##~~ command handlers + def _gcode_G1_sending(self, cmd, cmd_type=None): + cmd = self._replace_feedrate(cmd) + cmd = self._replace_intensity(cmd) + return cmd + + def _gcode_G2_sending(self, cmd, cmd_type=None): + cmd = self._replace_feedrate(cmd) + cmd = self._replace_intensity(cmd) + return cmd + + def _gcode_G3_sending(self, cmd, cmd_type=None): + cmd = self._replace_feedrate(cmd) + cmd = self._replace_intensity(cmd) + return cmd + + def _gcode_G01_sending(self, cmd, cmd_type=None): + return self._gcode_G1_sending(cmd, cmd_type) + + def _gcode_G02_sending(self, cmd, cmd_type=None): + return self._gcode_G2_sending(cmd, cmd_type) + + def _gcode_G03_sending(self, cmd, cmd_type=None): + return self._gcode_G3_sending(cmd, cmd_type) + + def _gcode_H_sent(self, cmd, cmd_type=None): + self._changeState(self.STATE_HOMING) + return cmd + + def _gcode_Hold_sent(self, cmd, cmd_type=None): + self._changeState(self.STATE_PAUSED) + return cmd + + def _gcode_Resume_sent(self, cmd, cmd_type=None): + self._changeState(self.STATE_PRINTING) + return cmd + + def sendCommand(self, cmd, cmd_type=None, processed=False): + cmd = cmd.encode('ascii', 'replace') + if not processed: + cmd = process_gcode_line(cmd) + if not cmd: + return + + if cmd[0] == "/": + specialcmd = cmd[1:].lower() + if "togglestatusreport" in specialcmd: + if self._status_timer is None: + self._status_timer = RepeatedTimer(1, self._poll_status) + self._status_timer.start() + else: + self._status_timer.cancel() + self._status_timer = None + elif "setstatusfrequency" in specialcmd: + data = specialcmd[18:] + try: + frequency = float(data) + except ValueError: + self._log("No frequency setting found! Using 1 sec.") + frequency = 1 + if self._status_timer is not None: + self._status_timer.cancel() + self._status_timer = RepeatedTimer(frequency, self._poll_status) + self._status_timer.start() + elif "disconnect" in specialcmd: + self.close() + elif "feedrate" in specialcmd: + data = specialcmd[8:] + self._set_feedrate_override(int(data)) + elif "intensity" in specialcmd: + data = specialcmd[9:] + self._set_intensity_override(int(data)) + else: + self._log("Command not Found! %s" % cmd) + self._log("available commands are:") + self._log(" /togglestatusreport") + self._log(" /setstatusfrequency ") + self._log(" /feedrate <%>") + self._log(" /intensity <%>") + self._log(" /disconnect") + return + + eepromCmd = re.search("^\$[0-9]+=.+$", cmd) + if(eepromCmd and self.isPrinting()): + self._log("Warning: Configuration changes during print are not allowed!") + + self._commandQueue.put(cmd) + self._send_event.set() + + def selectFile(self, filename, sd): + if self.isBusy(): + return + + self._currentFile = PrintingGcodeFileInformation(filename) + eventManager().fire(Events.FILE_SELECTED, { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + }) + self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False) + + def unselectFile(self): + if self.isBusy(): + return + + self._currentFile = None + eventManager().fire(Events.FILE_DESELECTED) + self._callback.on_comm_file_selected(None, None, False) + + def startPrint(self): + if not self.isOperational(): + return + + if self._currentFile is None: + raise ValueError("No file selected for printing") + + try: + # ensure fan is on whatever gcode follows. + self.sendCommand("M08") + + self._currentFile.start() + self._finished_currentFile = False + + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + eventManager().fire(Events.PRINT_STARTED, payload) + #self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload)) + + self._changeState(self.STATE_PRINTING) + except: + self._logger.exception("Error while trying to start printing") + self._errorValue = get_exception_string() + self._changeState(self.STATE_ERROR) + eventManager().fire(Events.ERROR, {"error": self.getErrorString()}) + + def cancelPrint(self): + if not self.isOperational(): + return + + with self._commandQueue.mutex: + self._commandQueue.queue.clear() + self._soft_reset() + self._acc_line_buffer = [] + self._send_event.clear(completely=True) + self._changeState(self.STATE_LOCKED) + + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + + eventManager().fire(Events.PRINT_CANCELLED, payload) + + def setPause(self, pause, send_cmd=True): + if not self._currentFile: + return + + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + + if not pause and self.isPaused(): + if self._pauseWaitStartTime: + self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime) + self._pauseWaitStartTime = None + self._pause_delay_time = time.time() + if send_cmd is True: + self._real_time_commands['cycle_start']=True + self._send_event.set() + eventManager().fire(Events.PRINT_RESUMED, payload) + elif pause and self.isPrinting(): + if not self._pauseWaitStartTime: + self._pauseWaitStartTime = time.time() + self._pause_delay_time = time.time() + if send_cmd is True: + self._real_time_commands['feed_hold']=True + self._send_event.set() + eventManager().fire(Events.PRINT_PAUSED, payload) + + def getStateString(self): + if self._state == self.STATE_NONE: + return "Offline" + if self._state == self.STATE_OPEN_SERIAL: + return "Opening serial port" + if self._state == self.STATE_DETECT_SERIAL: + return "Detecting serial port" + if self._state == self.STATE_CONNECTING: + return "Connecting" + if self._state == self.STATE_OPERATIONAL: + return "Operational" + if self._state == self.STATE_PRINTING: + return "Printing" + if self._state == self.STATE_PAUSED: + return "Paused" + if self._state == self.STATE_CLOSED: + return "Closed" + if self._state == self.STATE_ERROR: + return "Error: %s" % (self.getErrorString()) + if self._state == self.STATE_CLOSED_WITH_ERROR: + return "Error: %s" % (self.getErrorString()) + if self._state == self.STATE_LOCKED: + return "Locked" + if self._state == self.STATE_HOMING: + return "Homing" + if self._state == self.STATE_FLASHING: + return "Flashing" + return "?%d?" % (self._state) + + def getPrintProgress(self): + if self._currentFile is None: + return None + return self._currentFile.getProgress() + + def getPrintFilepos(self): + if self._currentFile is None: + return None + return self._currentFile.getFilepos() + + def getCleanedPrintTime(self): + printTime = self.getPrintTime() + if printTime is None: + return None + return printTime + + def getConnection(self): + return self._port, self._baudrate + + def isOperational(self): + return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED + + def isPrinting(self): + return self._state == self.STATE_PRINTING + + def isPaused(self): + return self._state == self.STATE_PAUSED + + def isLocked(self): + return self._state == self.STATE_LOCKED + + def isHoming(self): + return self._state == self.STATE_HOMING + + def isFlashing(self): + return self._state == self.STATE_FLASHING + + def isBusy(self): + return self.isPrinting() or self.isPaused() + + def isError(self): + return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR + + def isClosedOrError(self): + return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED + + def isSdReady(self): + return False + + def isStreaming(self): + return False + + def getErrorString(self): + return self._errorValue + + def getPrintTime(self): + if self._currentFile is None or self._currentFile.getStartTime() is None: + return None + else: + return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost + + def close(self, isError = False): + if self._status_timer is not None: + try: + self._status_timer.cancel() + self._status_timer = None + except AttributeError: + pass + + self._monitoring_active = False + self._sending_active = False + + printing = self.isPrinting() or self.isPaused() + if self._serial is not None: + if isError: + self._changeState(self.STATE_CLOSED_WITH_ERROR) + else: + self._changeState(self.STATE_CLOSED) + self._serial.close() + self._serial = None + + if printing: + payload = None + if self._currentFile is not None: + payload = { + "file": self._currentFile.getFilename(), + "filename": os.path.basename(self._currentFile.getFilename()), + "origin": self._currentFile.getFileLocation() + } + eventManager().fire(Events.PRINT_FAILED, payload) + eventManager().fire(Events.DISCONNECTED) + +### MachineCom callback ################################################################################################ +class MachineComPrintCallback(object): + def on_comm_log(self, message): + pass + + def on_comm_temperature_update(self, temp, bedTemp): + pass + + def on_comm_state_change(self, state): + pass + + def on_comm_message(self, message): + pass + + def on_comm_progress(self): + pass + + def on_comm_print_job_done(self): + pass + + def on_comm_z_change(self, newZ): + pass + + def on_comm_file_selected(self, filename, filesize, sd): + pass + + def on_comm_sd_state_change(self, sdReady): + pass + + def on_comm_sd_files(self, files): + pass + + def on_comm_file_transfer_started(self, filename, filesize): + pass + + def on_comm_file_transfer_done(self, filename): + pass + + def on_comm_force_disconnect(self): + pass + + def on_comm_pos_update(self, MPos, WPos): + pass + +class PrintingFileInformation(object): + """ + Encapsulates information regarding the current file being printed: file name, current position, total size and + time the print started. + Allows to reset the current file position to 0 and to calculate the current progress as a floating point + value between 0 and 1. + """ + + def __init__(self, filename): + self._logger = logging.getLogger(__name__) + self._filename = filename + self._pos = 0 + self._size = None + self._start_time = None + + def getStartTime(self): + return self._start_time + + def getFilename(self): + return self._filename + + def getFilesize(self): + return self._size + + def getFilepos(self): + return self._pos + + def getFileLocation(self): + return FileDestinations.LOCAL + + def getProgress(self): + """ + The current progress of the file, calculated as relation between file position and absolute size. Returns -1 + if file size is None or < 1. + """ + if self._size is None or not self._size > 0: + return -1 + return float(self._pos) / float(self._size) + + def reset(self): + """ + Resets the current file position to 0. + """ + self._pos = 0 + + def start(self): + """ + Marks the print job as started and remembers the start time. + """ + self._start_time = time.time() + + def close(self): + """ + Closes the print job. + """ + pass + +class PrintingGcodeFileInformation(PrintingFileInformation): + """ + Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures + that the file is closed in case of an error. + """ + + def __init__(self, filename, offsets_callback=None, current_tool_callback=None): + PrintingFileInformation.__init__(self, filename) + + self._handle = None + + self._first_line = None + + self._offsets_callback = offsets_callback + self._current_tool_callback = current_tool_callback + + if not os.path.exists(self._filename) or not os.path.isfile(self._filename): + raise IOError("File %s does not exist" % self._filename) + self._size = os.stat(self._filename).st_size + self._pos = 0 + + def start(self): + """ + Opens the file for reading and determines the file size. + """ + PrintingFileInformation.start(self) + self._handle = open(self._filename, "r") + + def close(self): + """ + Closes the file if it's still open. + """ + PrintingFileInformation.close(self) + if self._handle is not None: + try: + self._handle.close() + except: + pass + self._handle = None + + def getNext(self): + """ + Retrieves the next line for printing. + """ + if self._handle is None: + raise ValueError("File %s is not open for reading" % self._filename) + + try: + processed = None + while processed is None: + if self._handle is None: + # file got closed just now + return None + line = self._handle.readline() + if not line: + self.close() + processed = process_gcode_line(line) + self._pos = self._handle.tell() + + return processed + except Exception as e: + self.close() + self._logger.exception("Exception while processing line") + raise e + +def convert_pause_triggers(configured_triggers): + triggers = { + "enable": [], + "disable": [], + "toggle": [] + } + for trigger in configured_triggers: + if not "regex" in trigger or not "type" in trigger: + continue + + try: + regex = trigger["regex"] + t = trigger["type"] + if t in triggers: + # make sure regex is valid + re.compile(regex) + # add to type list + triggers[t].append(regex) + except re.error: + # invalid regex or something like this, we'll just skip this entry + pass + + result = dict() + for t in triggers.keys(): + if len(triggers[t]) > 0: + result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t]))) + return result + +def process_gcode_line(line): + line = strip_comment(line).strip() + line = line.replace(" ", "") + if not len(line): + return None + return line + +def strip_comment(line): + if not ";" in line: + # shortcut + return line + + escaped = False + result = [] + for c in line: + if c == ";" and not escaped: + break + result += c + escaped = (c == "\\") and not escaped + return "".join(result) + +def get_new_timeout(t): + now = time.time() + return now + get_interval(t) + +def get_interval(t): + if t not in default_settings["serial"]["timeout"]: + return 0 + else: + return settings().getFloat(["serial", "timeout", t]) + +def serialList(): + baselist = [] + baselist = baselist \ + + glob.glob("/dev/ttyUSB*") \ + + glob.glob("/dev/ttyACM*") \ + + glob.glob("/dev/tty.usb*") \ + + glob.glob("/dev/cu.*") \ + + glob.glob("/dev/cuaU*") \ + + glob.glob("/dev/rfcomm*") + + additionalPorts = settings().get(["serial", "additionalPorts"]) + for additional in additionalPorts: + baselist += glob.glob(additional) + + prev = settings().get(["serial", "port"]) + if prev in baselist: + baselist.remove(prev) + baselist.insert(0, prev) + if settings().getBoolean(["devel", "virtualPrinter", "enabled"]): + baselist.append("VIRTUAL") + return filter(None, baselist) + +def baudrateList(): + ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600] + prev = settings().getInt(["serial", "baudrate"]) + if prev in ret: + ret.remove(prev) + ret.insert(0, prev) + return ret