├── .gitignore ├── MANIFEST.in ├── README.md ├── babel.cfg ├── octoprint_astroprint ├── AstroprintCloud.py ├── AstroprintDB.py ├── SqliteDB.py ├── __init__.py ├── boxrouter │ ├── __init__.py │ ├── events.py │ └── handlers │ │ ├── __init__.py │ │ └── requesthandler.py ├── cameramanager │ └── __init__.py ├── downloadmanager │ └── __init__.py ├── gCodeAnalyzer │ └── __init__.py ├── materialcounter │ └── __init__.py ├── printerlistener │ └── __init__.py ├── static │ ├── css │ │ └── astroprint.css │ ├── img │ │ ├── Astroprint_square_logo.png │ │ └── favicon_rocket.ico │ ├── js │ │ └── astroprint.js │ └── less │ │ └── astroprint.less ├── templates │ ├── astroprint_settings.jinja2 │ └── astroprint_tab.jinja2 └── util │ └── AstroprintGCodeAnalyzer ├── requirements.txt ├── setup.py └── translations └── README.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea 4 | .vscode 5 | *.iml 6 | build 7 | dist 8 | .editorconfig 9 | *.egg* 10 | .DS_Store 11 | *.zip 12 | 13 | 14 | 15 | octoprint_astroprint/util/AstroprintGCodeAnalyzerLinux 16 | octoprint_astroprint/util/AstroprintGCodeAnalyzerMac 17 | 18 | 19 | *.gcode 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include octoprint_astroprint/templates * 3 | recursive-include octoprint_astroprint/translations * 4 | recursive-include octoprint_astroprint/static * 5 | recursive-include octoprint_astroprint/util * 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctoPrint Plugin: AstroPrint Cloud 2 | 3 | 4 | 5 | ## What is AstroPrint? 6 | 7 | AstroPrint is a 3D Printing Software Platform that makes 3D Printing easy and enjoyable. It includes, queing, file management, remote monitoring, video livestreaming, access to quality printable content, and more... 8 | 9 | You can access your 3D Printer via our mobile apps, desktop app or the AstroPrint Cloud at astroprint.com. 10 | 11 | More info or create an account at [astroprint.com](https://www.astroprint.com) 12 | 13 | ## What is this Plugin? 14 | 15 | This plugin allows you to link your OctoPrint-enabled 3D Printer to your AstroPrint account. Once the link is established, you will be able to: 16 | 17 | * Monitor your 3D Printer from anywhere. 18 | * Use the [AstroPrint Mobile App](https://www.astroprint.com/products/p/astroprint-mobile), [Desktop App](https://www.astroprint.com/products/p/astroprint-desktop) or [cloud](https://www.astroprint.com/products/p/astroprint-cloud) to start, pause or cancel print jobs. 19 | * Search for designs on Thingiverse, MyMiniFactory, and [Toy Maker](https://toymaker.astroprint.com) and print directly to your 3D Printer. 20 | 21 | ## How to Install 22 | 23 | Simply download the ***zip file*** from the [latest release](https://github.com/AstroPrint/OctoPrint-AstroPrint/releases/latest) and install it using your OctoPrint Plugin Manager. 24 | 25 | ## How to Use 26 | 27 | Once the plugin is installed, you will see an "AstroPrint" tab. 28 | 29 | The first thing you need to do is to link the OctoPrint device to your AstroPrint account. You need the Access Key on your AstroPrint Cloud Settings section. It's under Advanced Settings. After that, you can accept the connection and you should see the design files on your AstroPrint acount. 30 | 31 | Once the OctoPrint device is linked, your device will be part of your AstroPrint connected devices, you can use it with the mobile app, monitor from the cloud and start/stop jobs remotely. 32 | 33 | ## Mobile Apps 34 | 35 | Once your OctoPrint installation has been "AstroPrint-Enabled", you can download and use the AstroPrint Mobile app to control your 3D Printer locally and remotely. 36 | 37 | **[Download App Here](https://www.astroprint.com/products/p/astroprint-mobile)** 38 | 39 | **Note**: For the iOS app, make sure you eanbled CORS Access in the OctoPrint API Settings. 40 | 41 | ## Support 42 | 43 | You can use one of these channels: 44 | 45 | * [AstroPrint Forum](https://forum.astroprint.com/) 46 | * [Learning Center](https://astroprint.zendesk.com/hc/en-us) 47 | * [Support Ticket](https://astroprint.zendesk.com/hc/en-us/requests/new) -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: */**.py] 2 | [jinja2: */**.jinja2] 3 | extensions=jinja2.ext.autoescape, jinja2.ext.with_ 4 | 5 | [javascript: */**.js] 6 | extract_messages = gettext, ngettext 7 | -------------------------------------------------------------------------------- /octoprint_astroprint/AstroprintCloud.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | from flask import request, make_response, jsonify 9 | import time 10 | import json 11 | import octoprint.filemanager 12 | from .downloadmanager import DownloadManager 13 | from .boxrouter import boxrouterManager 14 | from requests_toolbelt import MultipartEncoder 15 | import requests 16 | import os 17 | import octoprint.filemanager.util 18 | from octoprint.filemanager.destinations import FileDestinations 19 | from octoprint.server import VERSION 20 | from threading import Lock 21 | import sys 22 | import platform 23 | 24 | class AstroprintCloud(): 25 | 26 | def __init__(self, plugin): 27 | self.plugin = None 28 | self.appId = None 29 | self.currentPrintingJob = None 30 | self.getTokenRefreshLock = Lock() 31 | 32 | settings = plugin.get_settings() 33 | 34 | self.plugin = plugin 35 | self.boxId = self.plugin.boxId 36 | self.apiHost = settings.get(["apiHost"]) 37 | self.appId = settings.get(["appId"]) 38 | self.db = self.plugin.db 39 | self.bm = boxrouterManager(self.plugin) 40 | self.downloadmanager = DownloadManager(self) 41 | self._logger = self.plugin.get_logger() 42 | self._printer = plugin.get_printer() 43 | self._file_manager = plugin.get_file_manager() 44 | self.plugin.cameraManager.astroprintCloud = self 45 | self.plugin.get_printer_listener().astroprintCloud = self 46 | self.statePayload = None 47 | self.printJobData = None 48 | self.sendJobInfo = False 49 | self._logger.info("CLEANBED: [%s]" % self.plugin.isBedClear) 50 | if self.plugin.user: 51 | self._logger.info("Found stored AstroPrint User [%s]" % self.plugin.user['name']) 52 | self.refresh() 53 | 54 | # We need to check again because the user variable might have been set to None as a consequence 55 | # of a invalid refresh token. 56 | if self.plugin.user: 57 | self.getUserInfo() 58 | self.getFleetInfo() 59 | else: 60 | self._logger.info("No stored AstroPrint user") 61 | 62 | 63 | def tokenIsExpired(self): 64 | return (self.plugin.user['expires'] - int(time.time()) < 60) 65 | 66 | def getToken(self): 67 | if self.tokenIsExpired(): 68 | with self.getTokenRefreshLock: 69 | # We need to check again because there could be calls that were waiting on the lock for an active refresh. 70 | # These calls should not have to refresh again as the token would be valid 71 | if self.tokenIsExpired(): 72 | self.refresh() 73 | 74 | return self.getToken() 75 | 76 | else: 77 | return self.plugin.user['token'] 78 | 79 | def getTokenRequestHeaders(self, contentType = 'application/x-www-form-urlencoded'): 80 | token = self.getToken() 81 | headers = { 82 | 'Content-Type': contentType, 83 | 'authorization': "bearer %s" % token 84 | } 85 | if(self.plugin.user['groupId']): 86 | headers['X-Org-Id'] = self.plugin.user['orgId'] 87 | headers['X-Group-Id'] = self.plugin.user['groupId'] 88 | 89 | return headers 90 | 91 | def refresh(self): 92 | try: 93 | r = requests.post( 94 | "%s/token" % (self.apiHost), 95 | data = { 96 | "client_id": self.appId, 97 | "grant_type": "refresh_token", 98 | "refresh_token": self.plugin.user['refresh_token'] 99 | }, 100 | ) 101 | r.raise_for_status() 102 | data = r.json() 103 | self.plugin.user['token'] = data['access_token'] 104 | self.plugin.user['refresh_token'] = data['refresh_token'] 105 | self.plugin.user['last_request'] = int(time.time()) 106 | self.plugin.user['expires'] = self.plugin.user['last_request'] + data['expires_in'] 107 | self.db.saveUser(self.plugin.user) 108 | self._logger.info("Astroprint Token refreshed") 109 | 110 | except requests.exceptions.HTTPError as err: 111 | if err.response.status_code == 400 or err.response.status_code == 401: 112 | self._logger.error("Unable to refresh token with error [%d]" % err.response.status_code) 113 | self.plugin.send_event("logOut") 114 | self.unauthorizedHandler() 115 | else: 116 | self._logger.error(err, exc_info=True) 117 | 118 | except requests.exceptions.RequestException as e: 119 | self._logger.error(e, exc_info=True) 120 | 121 | def loginAstroPrint(self, code, url, apAccessKey): 122 | self._logger.info("Logging into AstroPrint with boxId: %s" % self.boxId) 123 | try: 124 | r = requests.post( 125 | "%s/token" % (self.apiHost), 126 | data = { 127 | "client_id": self.appId, 128 | "access_key" : apAccessKey, 129 | "grant_type": "controller_authorization_code", 130 | "code": code, 131 | "redirect_uri": url, 132 | "box_id" : self.boxId 133 | }, 134 | ) 135 | r.raise_for_status() 136 | data = r.json() 137 | self.plugin.user = {} 138 | self.plugin.user['token'] = data['access_token'] 139 | self.plugin.user['refresh_token'] = data['refresh_token'] 140 | self.plugin.user['last_request'] = int(time.time()) 141 | self.plugin.user['accessKey'] = apAccessKey 142 | self.plugin.user['expires'] = self.plugin.user['last_request'] + data['expires_in'] 143 | self.plugin.user['email'] = None 144 | self.plugin.user['orgId'] = None 145 | self.plugin.user['groupId'] = None 146 | self.getFleetInfo() 147 | return self.getUserInfo(True) 148 | 149 | except requests.exceptions.HTTPError as err: 150 | self._logger.error("Error while logging into AstroPrint: %s" % err.response.text) 151 | return jsonify(json.loads(err.response.text)), err.response.status_code, {'ContentType':'application/json'} 152 | 153 | except requests.exceptions.RequestException as e: 154 | self._logger.error(e, exc_info=True) 155 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 156 | 157 | def getUserInfo(self, saveUser = False): 158 | self._logger.info("Getting AstroPrint user info") 159 | try: 160 | tokenHeaders = self.getTokenRequestHeaders('application/x-www-form-urlencoded') 161 | r = requests.get( 162 | "%s/accounts/me" % (self.apiHost), 163 | headers = tokenHeaders 164 | ) 165 | r.raise_for_status() 166 | data = r.json() 167 | self.plugin.user['id'] = data['id'] 168 | self.plugin.user['email'] = data['email'] 169 | self.plugin.user['name'] = data['name'] 170 | self.plugin.sendSocketInfo() 171 | if saveUser: 172 | self.db.saveUser(self.plugin.user) 173 | self._logger.info("AstroPrint User [%s] logged in and saved" % self.plugin.user['name']) 174 | self.connectBoxrouter() 175 | return jsonify({'name': self.plugin.user['name'], 'email': self.plugin.user['email']}), 200, {'ContentType':'application/json'} 176 | 177 | except requests.exceptions.HTTPError as err: 178 | if (err.response.status_code == 401): 179 | self.unauthorizedHandler() 180 | else: 181 | self._logger.error(err, exc_info=True) 182 | 183 | if saveUser: 184 | return jsonify(json.loads(err.response.text)), err.response.status_code, {'ContentType':'application/json'} 185 | except requests.exceptions.RequestException as e: 186 | self._logger.error(e, exc_info=True) 187 | if saveUser: 188 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 189 | 190 | def getFleetInfo(self): 191 | try: 192 | tokenHeaders = self.getTokenRequestHeaders('application/x-www-form-urlencoded') 193 | r = requests.get( 194 | "%s/devices/%s/fleet" % (self.apiHost, self.boxId), 195 | headers = tokenHeaders 196 | ) 197 | r.raise_for_status() 198 | data = r.json() 199 | if self.plugin.user['groupId'] != data['group_id']: 200 | self._logger.info("Box group id updated") 201 | self.plugin.user['orgId'] = data['organization_id'] 202 | self.plugin.user['groupId'] = data['group_id'] 203 | self.db.saveUser(self.plugin.user) 204 | 205 | except requests.exceptions.HTTPError as err: 206 | if (err.response.status_code == 401 or (err.response.status_code == 404 and self.plugin.user['groupId'])): 207 | self._logger.info("Box belongs to a fleet & group where the user does not have permissions. Logging out") 208 | self.unauthorizedHandler() 209 | else: 210 | self._logger.error("getFleetInfo failed with error %d" % err.response.status_code) 211 | 212 | except requests.exceptions.RequestException as e: 213 | self._logger.error(e, exc_info=True) 214 | 215 | def updateFleetInfo(self, orgId, groupId): 216 | if self.plugin.user['groupId'] != groupId: 217 | self.plugin.user['orgId'] = orgId 218 | self.plugin.user['groupId'] = groupId 219 | 220 | 221 | def logoutAstroPrint(self): 222 | self.unauthorizedHandler(False) 223 | return jsonify({"Success" : True }), 200, {'ContentType':'application/json'} 224 | 225 | def unauthorizedHandler (self, expired = True): 226 | if(expired): 227 | self._logger.warning("Unautorized token, AstroPrint user logged out.") 228 | self.db.deleteUser() 229 | self.currentPrintingJob = None 230 | self.disconnectBoxrouter() 231 | self.plugin.astroPrintUserLoggedOut() 232 | 233 | def printStarted(self, name, path): 234 | print_file = self.db.getPrintFileByOctoPrintPath(path) 235 | print_file_id = print_file.printFileId if print_file else None 236 | print_file_name = print_file.printFileName if print_file else name 237 | 238 | if self.printJobData: 239 | if self.printJobData['print_file'] == print_file_id: 240 | self.currentPrintingJob = self.printJobData['print_job_id'] 241 | self.updatePrintJob("started") 242 | else: 243 | self.printJobData = None 244 | self.startPrintJob(print_file_id, print_file_name) 245 | else: 246 | self.startPrintJob(print_file_id, print_file_name) 247 | 248 | def startPrintJob(self, print_file_id= None, print_file_name= None): 249 | try: 250 | tokenHeaders = self.getTokenRequestHeaders('application/x-www-form-urlencoded') 251 | data = { 252 | "box_id": self.plugin.boxId, 253 | "product_variant_id": self.plugin.get_settings().get(["product_variant_id"]), 254 | "name": print_file_name, 255 | } 256 | 257 | if print_file_id: 258 | data['print_file_id'] = print_file_id 259 | 260 | 261 | r = requests.post( 262 | "%s/print-jobs" % (self.apiHost), 263 | json = data, 264 | headers = tokenHeaders, 265 | stream=True 266 | ) 267 | 268 | data = r.json() 269 | self.currentPrintingJob = data['id'] 270 | 271 | except requests.exceptions.HTTPError as err: 272 | if (err.response.status_code == 401): 273 | self.unauthorizedHandler() 274 | self._logger.error("Failed to send print_job request: %s" % err.response.text) 275 | except requests.exceptions.RequestException as e: 276 | self._logger.error("Failed to send print_job request: %s" % e) 277 | finally: 278 | if self.sendJobInfo: 279 | self.sendJobInfo = False 280 | self.bm.triggerEvent('onDownloadComplete', {"id": print_file_id, "isBeingPrinted": True, 'printjob_id' : self.currentPrintingJob}) 281 | 282 | def updatePrintJob(self, status, totalConsumedFilament = None): 283 | try: 284 | tokenHeaders = self.getTokenRequestHeaders('application/x-www-form-urlencoded') 285 | data = {'status': status} 286 | 287 | if totalConsumedFilament: 288 | data['material_used'] = totalConsumedFilament 289 | 290 | requests.patch( 291 | "%s/print-jobs/%s" % (self.apiHost, self.currentPrintingJob), 292 | json = data, 293 | headers = tokenHeaders, 294 | stream=True 295 | ) 296 | 297 | except requests.exceptions.HTTPError as err: 298 | if (err.response.status_code == 401): 299 | self.unauthorizedHandler() 300 | self._logger.error("Failed to send print_job request: %s" % err.response.text) 301 | except requests.exceptions.RequestException as e: 302 | self._logger.error("Failed to send print_job request: %s" % e) 303 | 304 | def connectBoxrouter(self): 305 | if self.plugin.user and "accessKey" in self.plugin.user and "id" in self.plugin.user: 306 | self.bm.boxrouter_connect() 307 | #let the singleton be recreated again, so new credentials are taken into use 308 | global _instance 309 | _instance = None 310 | return True 311 | 312 | return False 313 | 314 | def disconnectBoxrouter(self): 315 | self.bm.boxrouter_disconnect() 316 | 317 | def sendCurrentData(self): 318 | printer = self.plugin.get_printer() 319 | payload = { 320 | 'operational': printer.is_operational(), 321 | 'printing': printer.is_paused() or printer.is_printing(), 322 | 'ready_to_print': self.plugin.isBedClear and printer.is_operational() and not printer.is_printing() and not printer.is_paused(), 323 | 'paused': self.plugin.get_printer().is_paused(), 324 | 'camera': True, #self.plugin.cameraManager.cameraActive 325 | 'heatingUp': self.plugin.printerIsHeating(), 326 | 'tool' : self.plugin.currentTool() 327 | } 328 | 329 | if self.statePayload != payload and self.bm: 330 | self.bm.broadcastEvent('status_update', payload) 331 | self.statePayload = payload 332 | 333 | 334 | def printFile(self, printFileId, printJobData = None, printNow = False): 335 | printFile = self.db.getPrintFileById(printFileId) 336 | if printNow: 337 | if not self.plugin.isBedClear: 338 | return None 339 | self.printJobData = printJobData 340 | if printFile and printNow: 341 | self.sendJobInfo = True 342 | self.printFileIsDownloaded(printFile) 343 | return "print" 344 | else: 345 | printFile = self.addPrintfileDownloadUrl(self.getPrintFileInfoForDownload(printFileId)) 346 | if printFile: 347 | printFile['printNow'] = printNow 348 | if not self.downloadmanager.isDownloading(printFileId): 349 | self.downloadmanager.startDownload(printFile) 350 | return "download" 351 | return None 352 | 353 | def getPrintFileInfoForDownload(self, printFileId): 354 | self._currentDownlading = printFileId 355 | self._downloading= True 356 | try: 357 | tokenHeaders = self.getTokenRequestHeaders() 358 | r = requests.get( 359 | "%s/printfiles/%s" % (self.apiHost, printFileId), 360 | headers= tokenHeaders, 361 | stream=True 362 | ) 363 | r.raise_for_status() 364 | printFile = r.json() 365 | if printFile['format'] == 'gcode' : 366 | return printFile 367 | else: 368 | payload = { 369 | "id" : printFileId, 370 | "type" : "error", 371 | "reason" : "Invalid file extension" 372 | } 373 | self.bm.triggerEvent('onDownload', payload) 374 | 375 | except requests.exceptions.HTTPError as err: 376 | payload = { 377 | "id" :printFileId, 378 | "type" : "error", 379 | "reason" : err.response.text 380 | } 381 | self.bm.triggerEvent('onDownload', payload) 382 | if (err.response.status_code == 401): 383 | self.unauthorizedHandler() 384 | return None 385 | except requests.exceptions.RequestException: 386 | payload = { 387 | "id" : printFile['id'], 388 | "type" : "error", 389 | } 390 | self.bm.triggerEvent('onDownload', payload) 391 | return None 392 | 393 | def addPrintfileDownloadUrl(self, printFile): 394 | if not printFile: 395 | return None 396 | try: 397 | tokenHeaders = self.getTokenRequestHeaders() 398 | r = requests.get( 399 | "%s/printfiles/%s/download?download_info=true" % (self.apiHost, printFile['id']), 400 | headers = tokenHeaders, 401 | stream=True 402 | ) 403 | r.raise_for_status() 404 | downloadInfo = r.json() 405 | printFile['download_url'] = downloadInfo['download_url'] 406 | 407 | return printFile 408 | 409 | except requests.exceptions.HTTPError as err: 410 | 411 | payload = { 412 | "id" : printFile['id'], 413 | "type" : "error", 414 | "reason" : err.response.text 415 | } 416 | self.bm.triggerEvent('onDownload', payload) 417 | if (err.response.status_code == 401): 418 | self.unauthorizedHandler() 419 | return None 420 | except requests.exceptions.RequestException: 421 | 422 | payload = { 423 | "id" : printFile['id'], 424 | "type" : "error", 425 | } 426 | 427 | self.bm.triggerEvent('onDownload', payload) 428 | return None 429 | 430 | 431 | def wrapAndSave(self, fileType, file, printNow=False): 432 | name = file if fileType == "design" else file.printFileName 433 | filepath = ("%s/%s" %(self.plugin._basefolder, name)) 434 | fileObject = octoprint.filemanager.util.DiskFileWrapper(name, filepath) 435 | 436 | try: 437 | self._file_manager.add_file(FileDestinations.LOCAL, name, fileObject, allow_overwrite=True) 438 | if fileType == "printFile": 439 | self.db.savePrintFile(file) 440 | if printNow: 441 | self.printFileIsDownloaded(file) 442 | return None 443 | 444 | except octoprint.filemanager.storage.StorageError as e: 445 | if os.path.exists(filepath): 446 | os.remove(filepath) 447 | if e.code == octoprint.filemanager.storage.StorageError.INVALID_FILE: 448 | payload = { 449 | "id" : file.printFileId, 450 | "type" : "error", 451 | "reason" : e.code 452 | } 453 | self.bm.triggerEvent('onDownload', payload) 454 | return None 455 | elif e.code == octoprint.filemanager.storage.StorageError.ALREADY_EXISTS: 456 | payload = { 457 | "id" : file.printFileId, 458 | "type" : "error", 459 | "reason" : e.code 460 | } 461 | self.bm.triggerEvent('onDownload', payload) 462 | return None 463 | else: 464 | payload = { 465 | "id" : file.printFileId, 466 | "type" : "error", 467 | } 468 | self.bm.triggerEvent('onDownload', payload) 469 | return None 470 | else: 471 | return None 472 | 473 | def printFileIsDownloaded(self, printFile): 474 | if self._printer.is_printing(): 475 | isBeingPrinted = False 476 | self.printJobData = None 477 | self.bm.triggerEvent('onDownloadComplete', {"id": printFile.printFileId, "isBeingPrinted": isBeingPrinted, 'printjob_id' : self.printJobData}) 478 | else: 479 | self._printer.select_file(self._file_manager.path_on_disk(FileDestinations.LOCAL, printFile.printFileName), False, True) 480 | if self._printer.is_printing(): 481 | self.sendJobInfo = True 482 | else: 483 | isBeingPrinted = False 484 | self.printJobData = None 485 | self.bm.triggerEvent('onDownloadComplete', {"id": printFile.printFileId, "isBeingPrinted": isBeingPrinted, 'printjob_id' : self.printJobData}) 486 | 487 | def getDesigns(self): 488 | try: 489 | tokenHeaders = self.getTokenRequestHeaders() 490 | r = requests.get( 491 | "%s/designs" % (self.apiHost), 492 | headers = tokenHeaders 493 | ) 494 | r.raise_for_status() 495 | data = r.json() 496 | return jsonify(data), 200, {'ContentType':'application/json'} 497 | 498 | except requests.exceptions.HTTPError as err: 499 | if (err.response.status_code == 401): 500 | self.unauthorizedHandler() 501 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 502 | except requests.exceptions.RequestException: 503 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 504 | 505 | def getDesignDownloadUrl(self, designId, name): 506 | try: 507 | tokenHeaders = self.getTokenRequestHeaders() 508 | r = requests.get( 509 | "%s/designs/%s/download" % (self.apiHost, designId), 510 | headers = tokenHeaders 511 | ) 512 | r.raise_for_status() 513 | data = r.json() 514 | 515 | self.downloadmanager.startDownload({"id": designId, "name" : name, "download_url" : data['download_url'], "designDownload" : True, "printNow" : False}) 516 | return jsonify({"Success" : True }), 200, {'ContentType':'application/json'} 517 | except requests.exceptions.HTTPError as err: 518 | if (err.response.status_code == 401): 519 | self.unauthorizedHandler() 520 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 521 | except requests.exceptions.RequestException: 522 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 523 | 524 | def getPrintFiles(self, designId): 525 | tokenHeaders = self.getTokenRequestHeaders() 526 | if designId: 527 | try: 528 | r = requests.get( 529 | "%s/designs/%s/printfiles" % (self.apiHost, designId), 530 | headers = tokenHeaders 531 | ) 532 | r.raise_for_status() 533 | data = r.json() 534 | return jsonify(data), 200, {'ContentType':'application/json'} 535 | 536 | except requests.exceptions.HTTPError as err: 537 | if (err.response.status_code == 401): 538 | self.unauthorizedHandler() 539 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 540 | except requests.exceptions.RequestException: 541 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 542 | else: 543 | try: 544 | r = requests.get( 545 | "%s/printfiles?design_id=null" % (self.apiHost), 546 | headers = tokenHeaders 547 | ) 548 | r.raise_for_status() 549 | data = r.json() 550 | return jsonify(data), 200, {'ContentType':'application/json'} 551 | 552 | except requests.exceptions.HTTPError as err: 553 | if (err.response.status_code == 401): 554 | self.unauthorizedHandler() 555 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 556 | except requests.exceptions.RequestException: 557 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 558 | 559 | def cancelDownload(self, printFileId): 560 | self.downloadmanager.cancelDownload(printFileId) 561 | 562 | 563 | def startPrintCapture(self, filename, filepath): 564 | data = {'name': filename} 565 | 566 | astroprint_print_file = self.db.getPrintFileByOctoPrintPath(filepath) 567 | 568 | if astroprint_print_file: 569 | data['print_file_id'] = astroprint_print_file.printFileId 570 | 571 | if self.currentPrintingJob: 572 | data['print_job_id'] = self.currentPrintingJob 573 | 574 | try: 575 | tokenHeaders = self.getTokenRequestHeaders() 576 | r = requests.post( 577 | "%s/timelapse" % self.apiHost, 578 | headers = tokenHeaders, 579 | stream=True, timeout= (10.0, 60.0), 580 | data = data 581 | ) 582 | 583 | status_code = r.status_code 584 | 585 | except: 586 | status_code = 500 587 | 588 | if status_code == 201: 589 | data = r.json() 590 | return { 591 | "error": False, 592 | "print_id": data['print_id'] 593 | } 594 | 595 | if status_code == 402: 596 | return { 597 | "error": "no_storage" 598 | } 599 | 600 | else: 601 | return { 602 | "error": "unable_to_create" 603 | } 604 | 605 | def uploadImageFile(self, print_id, imageBuf): 606 | try: 607 | m = MultipartEncoder(fields=[('file',('snapshot.jpg', imageBuf))]) 608 | tokenHeaders = self.getTokenRequestHeaders(m.content_type) 609 | r = requests.post( 610 | "%s/timelapse/%s/image" % (self.apiHost, print_id), 611 | data= m, 612 | headers= tokenHeaders 613 | ) 614 | m = None #Free the memory? 615 | status_code = r.status_code 616 | 617 | except requests.exceptions.HTTPError: 618 | status_code = 500 619 | except requests.exceptions.RequestException: 620 | status_code = 500 621 | 622 | if status_code == 201: 623 | data = r.json() 624 | return data 625 | else: 626 | return None 627 | 628 | def getManufacturer(self): 629 | try: 630 | r = requests.get( 631 | "%s/manufacturers" % (self.apiHost), 632 | headers={'Content-Type': 'application/x-www-form-urlencoded' } 633 | ) 634 | r.raise_for_status() 635 | data = r.json() 636 | return jsonify(data), 200, {'ContentType':'application/json'} 637 | 638 | except requests.exceptions.HTTPError as err: 639 | if (err.response.status_code == 401): 640 | self.unauthorizedHandler() 641 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 642 | except requests.exceptions.RequestException: 643 | 644 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 645 | 646 | def getManufacturerModels(self, manufacturer_id): 647 | try: 648 | r = requests.get( 649 | "%s/manufacturers/%s/models?format=gcode" % (self.apiHost, manufacturer_id), 650 | headers={'Content-Type': 'application/x-www-form-urlencoded' } 651 | ) 652 | r.raise_for_status() 653 | data = r.json() 654 | return jsonify(data), 200, {'ContentType':'application/json'} 655 | 656 | except requests.exceptions.HTTPError as err: 657 | if (err.response.status_code == 401): 658 | self.unauthorizedHandler() 659 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 660 | except requests.exceptions.RequestException: 661 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 662 | 663 | def getModelInfo(self, model_id): 664 | try: 665 | r = requests.get( 666 | "%s/manufacturers/models/%s" % (self.apiHost, model_id), 667 | headers={'Content-Type': 'application/x-www-form-urlencoded' } 668 | ) 669 | r.raise_for_status() 670 | data = r.json() 671 | return jsonify(data), 200, {'ContentType':'application/json'} 672 | 673 | except requests.exceptions.HTTPError as err: 674 | if (err.response.status_code == 401): 675 | self.unauthorizedHandler() 676 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 677 | except requests.exceptions.RequestException: 678 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 679 | 680 | def updateBoxrouterData(self, data): 681 | try: 682 | 683 | tokenHeaders = self.getTokenRequestHeaders('application/json') 684 | 685 | r = requests.patch( 686 | "%s/devices/%s/update-boxrouter-data" % (self.apiHost, self.plugin.boxId), 687 | headers = tokenHeaders, 688 | data=json.dumps(data) 689 | ) 690 | 691 | r.raise_for_status() 692 | 693 | return jsonify({'success': "Device Updated"}), 200, {'ContentType':'application/json'} 694 | 695 | except requests.exceptions.HTTPError as err: 696 | if (err.response.status_code == 401): 697 | self.unauthorizedHandler() 698 | return jsonify({'error': "Unauthorized user"}), err.response.status_code, {'ContentType':'application/json'} 699 | except requests.exceptions.RequestException: 700 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 701 | -------------------------------------------------------------------------------- /octoprint_astroprint/AstroprintDB.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import os 9 | import yaml 10 | import copy 11 | import codecs 12 | 13 | class AstroprintDB(): 14 | 15 | def __init__(self, plugin): 16 | dataFolder = plugin.get_plugin_data_folder() 17 | 18 | self.plugin = plugin 19 | self._logger = plugin.get_logger() 20 | self.infoPrintFiles = os.path.join(dataFolder,"print_files.yaml") 21 | self.printFiles = {} 22 | self.getPrintFiles() 23 | 24 | self.infoUser = os.path.join(dataFolder,"user.yaml") 25 | self.user = None 26 | self.getUser() 27 | 28 | def saveUser(self, user): 29 | # Copy the user object as we need the member unencrypted and the encryption operation below will modify the original object 30 | self.user = copy.copy(user) 31 | if user: 32 | user['email'] = encrypt(user['email']) if user['email'] else None 33 | user['accessKey'] = encrypt(user['accessKey']) 34 | user['orgId'] = encrypt(user['orgId']) if user['orgId'] else None 35 | user['groupId'] = encrypt(user['groupId']) if user['groupId'] else None 36 | 37 | with open(self.infoUser, "w") as infoFile: 38 | yaml.safe_dump({"user" : user}, infoFile, default_flow_style=False, indent=4, allow_unicode=True) 39 | 40 | self.plugin.user = self.user 41 | 42 | def getUser(self): 43 | try: 44 | with open(self.infoUser, "r") as f: 45 | user = yaml.safe_load(f) 46 | if user and user['user']: 47 | orgId = None 48 | groupId = None 49 | if 'orgId' in user: 50 | orgId = user['orgId'] 51 | if 'groupId' in user: 52 | groupId = user['groupId'] 53 | self.user = user['user'] 54 | self.user['email'] = decrypt(self.user['email']) 55 | self.user['accessKey'] = decrypt(self.user['accessKey']) 56 | self.user['orgId'] = decrypt(orgId) if orgId else None 57 | self.user['groupId'] = decrypt(groupId) if groupId else None 58 | 59 | except IOError as e: 60 | if e.errno == 2: 61 | self._logger.info("No user yaml: %s" % self.infoUser) 62 | else: 63 | self._logger.error("IOError error loading %s" % self.infoUser, exc_info= True) 64 | 65 | except: 66 | self._logger.error("There was an error loading %s" % self.infoUser, exc_info= True) 67 | 68 | self.plugin.user = self.user 69 | 70 | def deleteUser(self): 71 | self.saveUser(None) 72 | 73 | def getPrintFiles(self): 74 | try: 75 | with open(self.infoPrintFiles, "r") as f: 76 | printFiles = yaml.safe_load(f) 77 | if printFiles: 78 | self.printFiles = printFiles 79 | 80 | except IOError as e: 81 | if e.errno == 2: 82 | self._logger.info("No print files yaml: %s" % self.infoPrintFiles) 83 | else: 84 | self._logger.error("IOError error loading %s" % self.infoPrintFiles, exc_info= True) 85 | 86 | except: 87 | self._logger.info("There was an error loading %s" % self.infoPrintFiles, exc_info= True) 88 | 89 | self.plugin.printFiles = self.printFiles 90 | 91 | def savePrintFiles(self, printFiles): 92 | self.printFiles = printFiles 93 | with open(self.infoPrintFiles, "w") as infoFile: 94 | yaml.safe_dump(printFiles, infoFile, default_flow_style=False, indent=4, allow_unicode=True) 95 | self.plugin.printFiles = self.printFiles 96 | 97 | def savePrintFile(self, printFile): 98 | self.printFiles[printFile.printFileId] = {"name" : printFile.name, "octoPrintPath" : printFile.octoPrintPath, "printFileName" : printFile.printFileName, "renderedImage" : printFile.renderedImage} 99 | self.savePrintFiles(self.printFiles) 100 | 101 | def deletePrintFile(self, path): 102 | printFiles = {} 103 | if self.printFiles: 104 | for printFile in self.printFiles: 105 | if self.printFiles[printFile]["octoPrintPath"] != path: 106 | printFiles[printFile] = self.printFiles[printFile] 107 | self.savePrintFiles(printFiles) 108 | 109 | def getPrintFileById(self, printFileId): 110 | if self.printFiles and printFileId in self.printFiles: 111 | return AstroprintPrintFile(printFileId, self.printFiles[printFileId]["name"], self.printFiles[printFileId]["octoPrintPath"], self.printFiles[printFileId]["printFileName"], self.printFiles[printFileId]["renderedImage"]) 112 | return None 113 | 114 | 115 | def getPrintFileByOctoPrintPath(self, octoPrintPath): 116 | if self.printFiles: 117 | for printFile in self.printFiles: 118 | if self.printFiles[printFile]["octoPrintPath"] == octoPrintPath: 119 | return AstroprintPrintFile(printFile, self.printFiles[printFile]["name"], self.printFiles[printFile]["octoPrintPath"], self.printFiles[printFile]["printFileName"], self.printFiles[printFile]["renderedImage"]) 120 | return None 121 | 122 | class AstroprintPrintFile(): 123 | 124 | def __init__(self, printFileId = None, name="", octoPrintPath = "", printFileName="", renderedImage = None): 125 | self.printFileId = printFileId 126 | self.name = name 127 | self.octoPrintPath = octoPrintPath 128 | self.printFileName = printFileName 129 | self.renderedImage = renderedImage 130 | 131 | def encrypt(s): 132 | return codecs.encode(s, 'rot-13') 133 | 134 | def decrypt(s): 135 | return codecs.encode(s, 'rot-13') 136 | -------------------------------------------------------------------------------- /octoprint_astroprint/SqliteDB.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import sqlite3 9 | import os 10 | 11 | #KEEP FOR MIGRATION, IT WILL BE REMOVED AFTER SOME VERSIONS 12 | class SqliteDB(): 13 | def __init__(self, plugin): 14 | self.DB_NAME = os.path.join(plugin.get_plugin_data_folder(),"octoprint_astroprint.db") 15 | 16 | def execute(self, sql): 17 | conn = sqlite3.connect(self.DB_NAME) 18 | db = conn.cursor() 19 | db.execute(sql) 20 | conn.commit() 21 | conn.close() 22 | 23 | def getUser(self): 24 | conn = sqlite3.connect(self.DB_NAME) 25 | db = conn.cursor() 26 | sql = "SELECT * FROM user" 27 | db.execute(sql) 28 | user = db.fetchone() 29 | userData = {} 30 | 31 | if user: 32 | userData = {"name" : user[2], "email" : decrypt(user[3]) ,"token" : decrypt(user[4]), "refresh_token" : decrypt(user[5]), "accessKey" : decrypt(user[6]), "expires" : user[7], "last_request" : user[8]} 33 | else: 34 | userData = None 35 | return userData 36 | 37 | def getPrintFiles(self): 38 | conn = sqlite3.connect(self.DB_NAME) 39 | db = conn.cursor() 40 | sql = "SELECT * FROM printfile" 41 | db.execute(sql) 42 | printFiles = db.fetchall() 43 | allPrintFiles = {} 44 | for printFile in printFiles: 45 | allPrintFiles[printFile[0]] = {"name" : printFile[1], "octoPrintPath" : printFile[2], "printFileName" : printFile[3], "renderedImage" : printFile[4]} 46 | return allPrintFiles 47 | 48 | def decrypt(s): 49 | return s.encode('rot-13') 50 | -------------------------------------------------------------------------------- /octoprint_astroprint/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import octoprint.plugin 9 | import json 10 | import sys 11 | import socket 12 | import os 13 | import yaml 14 | import re 15 | import uuid 16 | 17 | 18 | from .AstroprintCloud import AstroprintCloud 19 | from .AstroprintDB import AstroprintDB 20 | from .SqliteDB import SqliteDB 21 | from .boxrouter import boxrouterManager 22 | from .cameramanager import cameraManager 23 | from .materialcounter import MaterialCounter 24 | from .printerlistener import PrinterListener 25 | 26 | from octoprint.server.util.flask import restricted_access 27 | from octoprint.server import admin_permission 28 | #When admin_permission is completly deprecated in future verions, use instead: 29 | #import octoprint.access.groups as groups 30 | #admin_permission = groups.GroupPermission(groups.ADMIN_GROUP) 31 | from octoprint.settings import valid_boolean_trues 32 | import octoprint_client 33 | 34 | from octoprint.users import SessionUser 35 | #When SessionUser from octoprint.users is completly deprecated in future verions, use instead: 36 | #from octoprint.access.users import SessionUser 37 | from octoprint.filemanager.destinations import FileDestinations 38 | from octoprint.events import Events 39 | 40 | from watchdog.observers import Observer 41 | 42 | from flask import request, Blueprint, make_response, jsonify, Response, abort 43 | from flask_login import user_logged_in, user_logged_out 44 | 45 | 46 | NO_CONTENT = ("", 204) 47 | OK = ("", 200) 48 | 49 | class JsonEncoder(json.JSONEncoder): 50 | def default(self, obj): 51 | return obj.__dict__ 52 | 53 | def getJsonCommandFromRequest(request, valid_commands): 54 | if not "application/json" in request.headers["Content-Type"]: 55 | return None, None, make_response("Expected content-type JSON", 400) 56 | 57 | data = request.json 58 | if not "command" in data.keys() or not data["command"] in valid_commands.keys(): 59 | return None, None, make_response("Expected valid command", 400) 60 | 61 | command = data["command"] 62 | for parameter in valid_commands[command]: 63 | if not parameter in data: 64 | return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400) 65 | 66 | return command, data, None 67 | 68 | def create_ws_token(public_key = None, api_key = None): 69 | from itsdangerous import URLSafeTimedSerializer 70 | 71 | s = URLSafeTimedSerializer(api_key) 72 | return s.dumps({ 'public_key': public_key }) 73 | 74 | 75 | class AstroprintPlugin(octoprint.plugin.SettingsPlugin, 76 | octoprint.plugin.AssetPlugin, 77 | octoprint.plugin.StartupPlugin, 78 | octoprint.plugin.TemplatePlugin, 79 | octoprint.plugin.BlueprintPlugin, 80 | octoprint.plugin.EventHandlerPlugin, 81 | octoprint.printer.PrinterCallback): 82 | 83 | ##~~ SettingsPlugin mixin 84 | 85 | def initialize(self): 86 | self.user = None 87 | self.designs = None 88 | self.db = None 89 | self.astroprintCloud = None 90 | self.cameraManager = None 91 | self.materialCounter= None 92 | self._printerListener = None 93 | self.groupId = None 94 | self.orgId = None 95 | self._boxId = None 96 | if self._settings.get(['clearBed']) == False: 97 | self._bed_clear = self._settings.get(['clearBed']) 98 | else : 99 | self._bed_clear = True 100 | 101 | def logOutHandler(sender, **kwargs): 102 | self.onLogout() 103 | 104 | def logInHandler(sender, **kwargs): 105 | for key, value in kwargs.items(): 106 | if isinstance(value, SessionUser): 107 | self.onLogin() 108 | 109 | self.logOutHandler = logOutHandler 110 | self.logInHandler = logInHandler 111 | 112 | user_logged_in.connect(logInHandler) 113 | user_logged_out.connect(logOutHandler) 114 | 115 | @property 116 | def boxId(self): 117 | if not self._boxId: 118 | import os 119 | 120 | boxIdFile = os.path.join(os.path.dirname(self._settings._configfile), "box-id") 121 | 122 | if os.path.exists(boxIdFile): 123 | with open(boxIdFile, 'r') as f: 124 | self._boxId = f.read().strip() 125 | 126 | if not self._boxId: 127 | self._boxId = uuid.uuid4().hex 128 | 129 | with open(boxIdFile, 'w') as f: 130 | f.write(self._boxId) 131 | 132 | return self._boxId 133 | 134 | @property 135 | def capabilities(self): 136 | capabilities = ['remotePrint', # Indicates whether this device supports starting a print job remotely 137 | 'multiExtruders', # Support for multiple extruders 138 | 'allowPrintFile', # Support for printing a printfile not belonging to any design 139 | 'acceptPrintJobId' # Accept created print job from cloud, 140 | ] 141 | if self._settings.get(["check_clear_bed"]) : 142 | capabilities.append('cleanState') # Support bed not clean state 143 | return capabilities 144 | 145 | def on_startup(self, host, port, *args, **kwargs): 146 | self._logger.info("Starting AstoPrint Plugin") 147 | self.register_printer_listener() 148 | self.db = AstroprintDB(self) 149 | if not self._settings.get(['check_clear_bed']): 150 | self.set_bed_clear(True) 151 | 152 | ## Move old mysql database data to new yaml file for logged users 153 | oldDbFile = os.path.join(self.get_plugin_data_folder(),"octoprint_astroprint.db") 154 | if os.path.isfile(oldDbFile): 155 | sqlitledb = SqliteDB(self) 156 | self.db.saveUser(sqlitledb.getUser()) 157 | self.db.savePrintFiles(sqlitledb.getPrintFiles()) 158 | os.remove(oldDbFile) 159 | 160 | self.cameraManager = cameraManager(self) 161 | self.astroprintCloud = AstroprintCloud(self) 162 | 163 | if self.user: 164 | self.astroprintCloud.connectBoxrouter() 165 | self.cameraManager.astroprintCloud = self.astroprintCloud 166 | self.materialCounter = MaterialCounter(self) 167 | baseurl = octoprint_client.build_base_url(host, port) 168 | self._logger.info("AstoPrint Plugin started, avalible in %s" % baseurl ) 169 | 170 | def onLogout(self): 171 | self.send_event("userLoggedOut", True) 172 | 173 | def onLogin(self): 174 | self.send_event("userLogged", True) 175 | 176 | def on_shutdown(self): 177 | #clear al process we created 178 | self.cameraManager.shutdown() 179 | self.astroprintCloud.downloadmanager.shutdown() 180 | self.unregister_printer_listener() 181 | 182 | def get_logger(self): 183 | return self._logger 184 | 185 | def get_printer(self): 186 | return self._printer 187 | 188 | @property 189 | def isBedClear(self): 190 | if self._settings.get(['check_clear_bed']): 191 | return self._bed_clear 192 | else: 193 | return True 194 | 195 | def set_bed_clear(self, clear, sendUpdate = False): 196 | if clear != self._bed_clear: 197 | self._bed_clear = clear 198 | self._settings.set(['bedClear'], clear) 199 | self._settings.save() 200 | self.send_event("bedclear", clear) 201 | if sendUpdate and self.astroprintCloud and self.astroprintCloud.bm: 202 | self.astroprintCloud.sendCurrentData() 203 | 204 | def get_printer_listener(self): 205 | return self._printerListener 206 | 207 | def get_settings(self): 208 | return self._settings 209 | 210 | def get_plugin_version(self): 211 | return self._plugin_version 212 | 213 | def get_file_manager(self): 214 | return self._file_manager 215 | 216 | def sendSocketInfo(self): 217 | data = { 218 | 'heatingUp' : self.printerIsHeating(), 219 | 'currentLayer' : self._printerListener.get_current_layer() if self._printerListener else None, 220 | 'camera' : self.cameraManager.cameraActive if self.cameraManager else None, 221 | 'userLogged' : self.user['email'] if self.user else None, 222 | 'job' : self._printerListener.get_job_data() if self._printerListener else None 223 | } 224 | self.send_event("socketUpdate", data) 225 | 226 | def astroPrintUserLoggedOut(self): 227 | self.send_event("astroPrintUserLoggedOut") 228 | 229 | def send_event(self, event, data=None): 230 | event = {'event':event, 'data':data} 231 | self._plugin_manager.send_plugin_message(self._plugin_name, event) 232 | 233 | def get_settings_defaults(self): 234 | 235 | appSite ="https://cloud.astroprint.com" 236 | appId="c4f4a98519194176842567680239a4c3" 237 | apiHost="https://api.astroprint.com/v2" 238 | webSocket="wss://boxrouter.astroprint.com" 239 | product_variant_id = "9e33c7a4303348e0b08714066bcc2750" 240 | boxName = socket.gethostname() 241 | 242 | try: 243 | with open(os.path.join(self.get_plugin_data_folder(), "config.yaml"), "r") as f: 244 | config = yaml.safe_load(f) 245 | if config: 246 | appSite = config['appSite'] 247 | appId = config['appId'] 248 | apiHost = config['apiHost'] 249 | webSocket = config['webSocket'] 250 | product_variant_id = config['product_variant_id'] 251 | except IOError as e: 252 | if e.errno != 2: 253 | self._logger.error("IOError error loading config.yaml", exc_info= True) 254 | 255 | except: 256 | self._logger.error("There was an error loading config.yaml", exc_info= True) 257 | 258 | return dict( 259 | #AstroPrintEndPoint 260 | appSite = appSite, 261 | appId = appId, 262 | apiHost = apiHost, 263 | webSocket = webSocket, 264 | product_variant_id = product_variant_id, 265 | boxName = boxName, 266 | printerModel = {'id' : None, 'name' : None}, 267 | filament = {'name' : None, 'color' : None}, 268 | camera = False, 269 | bedClear = True, 270 | check_clear_bed = True, 271 | #Adittional printer settings 272 | max_nozzle_temp = 280, #only for being set by AstroPrintCloud, it wont affect octoprint settings 273 | max_bed_temp = 140, 274 | ) 275 | 276 | def get_template_vars(self): 277 | 278 | return dict(appSite = self._settings.get(["appSite"]), 279 | appId = self._settings.get(["appId"]), 280 | appiHost = self._settings.get(["apiHost"]), 281 | boxName = self._settings.get(["boxName"]), 282 | boxId = self.boxId, 283 | clearBed = self._bed_clear, 284 | printerModel = json.dumps(self._settings.get(["printerModel"])) if self._settings.get(["printerModel"])['id'] else "null", 285 | filament = json.dumps(self._settings.get(["filament"])) if self._settings.get(["filament"])['name'] else "null", 286 | user = json.dumps({'name': self.user['name'], 'email': self.user['email']}, cls=JsonEncoder, indent=4) if self.user else None , 287 | camera = self._settings.get(["camera"]) 288 | ) 289 | 290 | 291 | def get_template_configs(self): 292 | return [ 293 | dict(type="navbar", custom_bindings=False), 294 | dict(type="settings", custom_bindings=True) 295 | ] 296 | 297 | ##~~ AssetPlugin mixin 298 | 299 | def get_assets(self): 300 | # Define your plugin's asset files to automatically include in the 301 | # core UI here. 302 | return dict( 303 | js=["js/astroprint.js"], 304 | css=["css/astroprint.css"], 305 | less=["less/astroprint.less"] 306 | ) 307 | 308 | ##~~ Softwareupdate hook 309 | 310 | def get_update_information(self): 311 | # Define the configuration for your plugin to use with the Software Update 312 | # Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update 313 | # for details. 314 | return dict( 315 | astroprint=dict( 316 | displayName="AstroPrint", 317 | displayVersion=self._plugin_version, 318 | 319 | # version check: github repository 320 | type="github_release", 321 | user="AstroPrint", 322 | repo="OctoPrint-AstroPrint", 323 | current=self._plugin_version, 324 | 325 | # update method: pip 326 | pip="https://github.com/AstroPrint/OctoPrint-AstroPrint/archive/{target_version}.zip" 327 | ) 328 | ) 329 | 330 | def register_printer_listener(self): 331 | self._printerListener = PrinterListener(self) 332 | self._printer.register_callback(self._printerListener) 333 | 334 | def unregister_printer_listener(self): 335 | self._printer.unregister_callback(self._printerListener) 336 | self._printerListener = None 337 | 338 | def on_event(self, event, payload): 339 | 340 | printEvents = [ 341 | Events.CONNECTED, 342 | Events.DISCONNECTED, 343 | Events.PRINT_STARTED, 344 | Events.PRINT_DONE, 345 | Events.PRINT_FAILED, 346 | Events.PRINT_CANCELLED, 347 | Events.PRINT_PAUSED, 348 | Events.PRINT_RESUMED, 349 | Events.ERROR, 350 | Events.TOOL_CHANGE 351 | ] 352 | 353 | cameraSuccessEvents = [ 354 | Events.CAPTURE_DONE, 355 | Events.POSTROLL_END, 356 | Events.MOVIE_RENDERING, 357 | Events.MOVIE_DONE, 358 | ] 359 | 360 | cameraFailEvents = [ 361 | Events.CAPTURE_FAILED, 362 | Events.MOVIE_FAILED 363 | ] 364 | 365 | if event in cameraSuccessEvents: 366 | self.cameraManager.cameraConnected() 367 | 368 | elif event in cameraFailEvents: 369 | self.cameraManager.cameraError() 370 | 371 | elif event == Events.FILE_REMOVED: 372 | if payload['storage'] == 'local': 373 | self.astroprintCloud.db.deletePrintFile(payload['path']) 374 | 375 | elif event == Events.CONNECTED: 376 | self.send_event("canPrint", True) 377 | 378 | elif event == Events.PRINT_CANCELLED or event == Events.PRINT_FAILED: 379 | self.send_event("canPrint", True) 380 | if self._settings.get(['check_clear_bed']): 381 | self.set_bed_clear(False) 382 | if self.user and self.astroprintCloud.currentPrintingJob: 383 | self.astroprintCloud.updatePrintJob("failed", self.materialCounter.totalConsumedFilament) 384 | self.astroprintCloud.currentPrintingJob = None 385 | self.cameraManager.stop_timelapse() 386 | self._analyzed_job_layers = None 387 | 388 | elif event == Events.PRINT_DONE: 389 | if self._settings.get(['check_clear_bed']): 390 | self.set_bed_clear(False) 391 | if self.user and self.astroprintCloud.currentPrintingJob: 392 | self.astroprintCloud.updatePrintJob("success", self.materialCounter.totalConsumedFilament) 393 | self.astroprintCloud.currentPrintingJob = None 394 | self.cameraManager.stop_timelapse() 395 | self.send_event("canPrint", True) 396 | 397 | elif event == Events.PRINT_STARTED: 398 | self.send_event("canPrint", False) 399 | if self.user: 400 | self.astroprintCloud.printStarted(payload['name'], payload['path']) 401 | 402 | self.materialCounter.startPrint() 403 | file = self._file_manager.path_on_disk(FileDestinations.LOCAL, payload['path']) 404 | self._printerListener.startPrint(file) 405 | if event in printEvents: 406 | self.sendSocketInfo() 407 | if self.user and self.astroprintCloud: 408 | self.astroprintCloud.sendCurrentData() 409 | 410 | return 411 | 412 | def printerIsHeating(self): 413 | heating = False 414 | if self._printer.is_operational(): 415 | heating = self._printer._comm._heating 416 | 417 | return heating 418 | 419 | def currentTool(self): 420 | tool = None 421 | if self._printer.is_operational(): 422 | tool = self._printer._comm._currentTool 423 | 424 | return tool 425 | 426 | def count_material(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): 427 | if self.materialCounter: 428 | if (gcode): 429 | gcodeHandler = "_gcode_" + gcode 430 | if hasattr(self.materialCounter, gcodeHandler): 431 | materialCounter = getattr(self.materialCounter, gcodeHandler,None) 432 | materialCounter(cmd) 433 | 434 | 435 | def is_blueprint_protected(self): 436 | return False 437 | 438 | @octoprint.plugin.BlueprintPlugin.route("/login", methods=["POST"]) 439 | @admin_permission.require(403) 440 | def login(self): 441 | return self.astroprintCloud.loginAstroPrint( 442 | request.json['code'], 443 | request.json['url'], 444 | request.json['ap_access_key'] 445 | ) 446 | 447 | @octoprint.plugin.BlueprintPlugin.route("/logout", methods=["POST"]) 448 | @admin_permission.require(403) 449 | def logout(self): 450 | return self.astroprintCloud.logoutAstroPrint() 451 | 452 | 453 | @octoprint.plugin.BlueprintPlugin.route("/designs", methods=["GET"]) 454 | @admin_permission.require(403) 455 | def getDesigns(self): 456 | return self.astroprintCloud.getDesigns() 457 | 458 | @octoprint.plugin.BlueprintPlugin.route("/printfiles", methods=["GET"]) 459 | @admin_permission.require(403) 460 | def getPrintFiles(self): 461 | designId = request.args.get('designId', None) 462 | return self.astroprintCloud.getPrintFiles(designId) 463 | 464 | @octoprint.plugin.BlueprintPlugin.route("/downloadDesign", methods=["POST"]) 465 | @admin_permission.require(403) 466 | def downloadDesign(self): 467 | designId = request.json['designId'] 468 | name = request.json['name'] 469 | return self.astroprintCloud.getDesignDownloadUrl(designId, name) 470 | 471 | @octoprint.plugin.BlueprintPlugin.route("/downloadPrintFile", methods=["POST"]) 472 | @admin_permission.require(403) 473 | def downloadPrintFile(self): 474 | printFileId = request.json['printFileId'] 475 | printNow = request.json['printNow'] 476 | if(printNow and not self.isBedClear): 477 | return jsonify({"error" : "Bed is not clean"}), 500, {'ContentType':'application/json'} 478 | if self.astroprintCloud.printFile(printFileId, printNow) == "print": 479 | return jsonify({"state" : "printing"}), 200, {'ContentType':'application/json'} 480 | if self.astroprintCloud.printFile(printFileId, printNow) == "download": 481 | return jsonify({"state" : "downloading"}), 200, {'ContentType':'application/json'} 482 | return jsonify({'error': "Internal server error"}), 500, {'ContentType':'application/json'} 483 | 484 | @octoprint.plugin.BlueprintPlugin.route("/clearbed", methods=["POST"]) 485 | @admin_permission.require(403) 486 | def clearBed(self): 487 | self.set_bed_clear(True, True) 488 | return jsonify({"clearBed" : "True"}), 200, {'ContentType':'application/json'} 489 | 490 | @octoprint.plugin.BlueprintPlugin.route("/canceldownload", methods=["POST"]) 491 | @admin_permission.require(403) 492 | def canceldownload(self): 493 | id = request.json['id'] 494 | self.astroprintCloud.downloadmanager.cancelDownload(id) 495 | return jsonify({"success" : True }), 200, {'ContentType':'application/json'} 496 | 497 | @octoprint.plugin.BlueprintPlugin.route("/checkcamerastatus", methods=["GET"]) 498 | @admin_permission.require(403) 499 | def checkcamerastatus(self): 500 | self.cameraManager.checkCameraStatus() 501 | return jsonify({"connected" : True if self.cameraManager.cameraActive else False }), 200, {'ContentType':'application/json'} 502 | 503 | @octoprint.plugin.BlueprintPlugin.route("/isloggeduser", methods=["GET"]) 504 | @admin_permission.require(403) 505 | def isloggeduser(self): 506 | if self.user: 507 | return jsonify({"user" : {"name" : self.user['name'], "email" : self.user['email']}}), 200, {'ContentType':'application/json'} 508 | else: 509 | return jsonify({"user" : False }), 200, {'ContentType':'application/json'} 510 | 511 | @octoprint.plugin.BlueprintPlugin.route("/iscameraconnected", methods=["GET"]) 512 | @admin_permission.require(403) 513 | def iscameraconnected(self): 514 | return jsonify({"connected" : True if self.cameraManager.cameraActive else False }), 200, {'ContentType':'application/json'} 515 | 516 | @octoprint.plugin.BlueprintPlugin.route("/connectboxrouter", methods=["POST"]) 517 | @admin_permission.require(403) 518 | def connectboxrouter(self): 519 | if self.astroprintCloud and self.astroprintCloud.bm: 520 | self.astroprintCloud.bm.boxrouter_connect() 521 | return jsonify({"connecting" : True }), 200, {'ContentType':'application/json'} 522 | 523 | @octoprint.plugin.BlueprintPlugin.route("/initialstate", methods=["GET"]) 524 | @admin_permission.require(403) 525 | def initialstate(self): 526 | try: 527 | return jsonify({ 528 | "user" : {"name" : self.user['name'], "email" : self.user['email']} if self.user else None, 529 | "connected" : self.cameraManager.cameraActive if self.cameraManager else None, 530 | "can_print" : True if self._printer.is_operational() and not (self._printer.is_paused() or self._printer.is_printing()) else False, 531 | "boxrouter_status" : self.astroprintCloud.bm.status if self.astroprintCloud and self.astroprintCloud.bm else "disconnected" 532 | }), 200, {'ContentType':'application/json'} 533 | 534 | except Exception as e: 535 | self._logger.error(e, exc_info=True) 536 | raise e 537 | 538 | @octoprint.plugin.BlueprintPlugin.route("/changename", methods=["POST"]) 539 | @admin_permission.require(403) 540 | def changeboxroutername(self): 541 | name = request.json['name'] 542 | self._settings.set(['boxName'], name) 543 | self._settings.save() 544 | if self.astroprintCloud and self.astroprintCloud.bm: 545 | data = { 546 | "name": name 547 | } 548 | return self.astroprintCloud.updateBoxrouterData(data) 549 | else: 550 | return jsonify({"connecting" : True }), 200, {'ContentType':'application/json'} 551 | 552 | @octoprint.plugin.BlueprintPlugin.route("/manufactures", methods=["GET"]) 553 | @admin_permission.require(403) 554 | def getManufacturers(self): 555 | return self.astroprintCloud.getManufacturer() 556 | 557 | @octoprint.plugin.BlueprintPlugin.route("/manufacturermodels", methods=["GET"]) 558 | @admin_permission.require(403) 559 | def getManufacturerModels(self): 560 | manufacturerId = request.args.get('manufacturerId', None) 561 | return self.astroprintCloud.getManufacturerModels(manufacturerId) 562 | 563 | @octoprint.plugin.BlueprintPlugin.route("/manufacturermodelinfo", methods=["GET"]) 564 | @admin_permission.require(403) 565 | def getModelInfo(self): 566 | modelId = request.args.get('modelId', None) 567 | return self.astroprintCloud.getPrintFiles(modelId) 568 | 569 | @octoprint.plugin.BlueprintPlugin.route("/changeprinter", methods=["POST"]) 570 | @admin_permission.require(403) 571 | def changeprinter(self): 572 | printer = request.json['printerModel'] 573 | self._settings.set(['printerModel'], printer) 574 | self._settings.save() 575 | data = { 576 | "printerModel": printer 577 | } 578 | return self.astroprintCloud.updateBoxrouterData(data) 579 | 580 | @octoprint.plugin.BlueprintPlugin.route("/changeprinter", methods=["DELETE"]) 581 | @admin_permission.require(403) 582 | def deleteprinter(self): 583 | self._settings.set(['printerModel'], {'id' : None, 'name' : None}) 584 | self._settings.save() 585 | data = { 586 | "printerModel": None 587 | } 588 | return self.astroprintCloud.updateBoxrouterData(data) 589 | 590 | @octoprint.plugin.BlueprintPlugin.route("/changefilament", methods=["POST"]) 591 | @admin_permission.require(403) 592 | def changefilament(self): 593 | filament = request.json['filament'] 594 | self._settings.set(['filament'], filament) 595 | self._settings.save() 596 | self.astroprintCloud.bm.triggerEvent('filamentChanged', {'filament' : filament}) 597 | return jsonify({"Filament updated" : True }), 200, {'ContentType':'application/json'} 598 | 599 | @octoprint.plugin.BlueprintPlugin.route("/changefilament", methods=["DELETE"]) 600 | @admin_permission.require(403) 601 | def removefilament(self): 602 | self._settings.set(['filament'], {'name' : None, 'color' : None}) 603 | self._settings.save() 604 | self.astroprintCloud.bm.triggerEvent('filamentChanged', {'filament' : {'name' : None, 'color' : None}}) 605 | return jsonify({"Filament removed" : True }), 200, {'ContentType':'application/json'} 606 | 607 | ##LOCAL AREA 608 | #Functions related to local aspects 609 | 610 | @octoprint.plugin.BlueprintPlugin.route("/astrobox/identify", methods=["GET"]) 611 | def identify(self): 612 | if not self.astroprintCloud or not self.astroprintCloud.bm: 613 | abort(503) 614 | return Response(json.dumps({ 615 | 'id': self.boxId, 616 | 'name': self._settings.get(["boxName"]), 617 | 'version': self._plugin_version, 618 | 'firstRun': True if self._settings.global_get_boolean(["server", "firstRun"]) else None, 619 | 'online': True, 620 | }) 621 | ) 622 | 623 | @octoprint.plugin.BlueprintPlugin.route("/accessKeys", methods=["POST"]) 624 | def getAccessKeys(self): 625 | email = request.values.get('email', None) 626 | accessKey = request.values.get('accessKey', None) 627 | 628 | if not email or not accessKey: 629 | abort(401) # wouldn't a 400 make more sense here? 630 | 631 | if self.user and self.user['email'] == email and self.user['accessKey'] == accessKey and self.user['id']: 632 | # only respond positively if we have an AstroPrint user and their mail AND accessKey match AND 633 | # they also have a valid id 634 | apiKey = self._settings.global_get(["api", "key"]) 635 | return jsonify(api_key=apiKey, 636 | ws_token=create_ws_token(self.user['id'], apiKey)) 637 | 638 | if not self.user: 639 | abort (401) 640 | # everyone else gets the cold shoulder 641 | abort(403) 642 | 643 | @octoprint.plugin.BlueprintPlugin.route("/status", methods=["GET"]) 644 | @admin_permission.require(403) 645 | def getStatus(self): 646 | 647 | fileName = None 648 | 649 | if self._printer.is_printing(): 650 | currentJob = self._printer.get_current_job() 651 | fileName = currentJob["file"]["name"] 652 | 653 | return Response( 654 | json.dumps({ 655 | 'id': self.boxId, 656 | 'name': self._settings.get(["boxName"]), 657 | 'printing': self._printer.is_printing() or self._printer.is_paused(), 658 | 'fileName': fileName, 659 | 'printerModel': self._settings.get(["printerModel"]) if self._settings.get(['printerModel'])['id'] else None, 660 | 'filament' : self._settings.get(["filament"]), 661 | 'material': None, 662 | 'operational': self._printer.is_operational(), 663 | 'ready_to_print': self.isBedClear and self._printer.is_operational() and not (self._printer.is_printing() or self._printer.is_paused()), 664 | "flipV" : self._settings.global_get(["webcam", "flipV"]), 665 | 'flipH' : self._settings.global_get(["webcam", "flipH"]), 666 | "rotate90" : self._settings.global_get(["webcam", "rotate90"]), 667 | 'paused': self._printer.is_paused(), 668 | 'camera': True, #self.cameraManager.cameraActive, 669 | 'remotePrint': True, 670 | 'capabilities': self.capabilities 671 | + self.cameraManager.capabilities 672 | }), 673 | mimetype= 'application/json' 674 | ) 675 | 676 | @octoprint.plugin.BlueprintPlugin.route("/api/printer-profile", methods=["GET"]) 677 | @admin_permission.require(403) 678 | def printer_profile_patch(self): 679 | printerProfile = self._printer_profile_manager.get_current_or_default() 680 | profile = { 681 | 'driver': "marlin", #At the moment octopi only supports marlin 682 | 'extruder_count': printerProfile['extruder']['count'], 683 | 'max_nozzle_temp': self._settings.get(["max_nozzle_temp"]), 684 | 'max_bed_temp': self._settings.get(["max_bed_temp"]), 685 | 'heated_bed': printerProfile['heatedBed'], 686 | 'cancel_gcode': ['G28 X0 Y0'],#ToDo figure out how to get it from snipet 687 | 'invert_z': printerProfile['axes']['z']['inverted'], 688 | 'printerModel': self._settings.get(["printerModel"]) if self._settings.get(['printerModel'])['id'] else None, 689 | 'filament' : self._settings.get(["filament"]) 690 | } 691 | return jsonify(profile) 692 | 693 | @octoprint.plugin.BlueprintPlugin.route('/api/astroprint', methods=['DELETE']) 694 | @admin_permission.require(403) 695 | def astroPrint_logout(self): 696 | return self.astroprintCloud.logoutAstroPrint() 697 | 698 | @octoprint.plugin.BlueprintPlugin.route('/api/job', methods=['GET']) 699 | @admin_permission.require(403) 700 | def jobState(self): 701 | currentData = self._printer.get_current_data() 702 | return jsonify({ 703 | "job": currentData["job"], 704 | "progress": currentData["progress"], 705 | "state": currentData["state"]["text"] 706 | }) 707 | 708 | # If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py 709 | # ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that 710 | # can be overwritten via __plugin_xyz__ control properties. See the documentation for that. 711 | __plugin_name__ = "AstroPrint" 712 | __plugin_pythoncompat__ = ">=3,<4" # Only Python 3 713 | 714 | def __plugin_load__(): 715 | global __plugin_implementation__ 716 | __plugin_implementation__ = AstroprintPlugin() 717 | 718 | global __plugin_hooks__ 719 | __plugin_hooks__ = { 720 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, 721 | "octoprint.comm.protocol.gcode.sent": __plugin_implementation__.count_material 722 | } 723 | -------------------------------------------------------------------------------- /octoprint_astroprint/boxrouter/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | # singleton 9 | _instance = None 10 | 11 | def boxrouterManager(plugin): 12 | global _instance 13 | if _instance is None: 14 | _instance = AstroprintBoxRouter(plugin) 15 | return _instance 16 | 17 | import json 18 | import threading 19 | import socket 20 | import os 21 | import sys 22 | import weakref 23 | import uuid 24 | 25 | from time import sleep, time 26 | 27 | from ws4py.client.threadedclient import WebSocketClient 28 | from ws4py.messaging import PingControlMessage 29 | 30 | import octoprint.util 31 | 32 | from .handlers import BoxRouterMessageHandler 33 | from .events import EventSender 34 | 35 | LINE_CHECK_STRING = 'box' 36 | 37 | class AstroprintBoxRouterClient(WebSocketClient): 38 | def __init__(self, hostname, router, plugin): 39 | self.connected = False 40 | self._printerListener = plugin.get_printer_listener() 41 | self._printer = plugin.get_printer() 42 | self._lastReceived = 0 43 | self._lineCheck = None 44 | self._error = False 45 | self._weakRefRouter = weakref.ref(router) 46 | self.plugin = plugin 47 | self._logger = self.plugin.get_logger() 48 | self._condition = threading.Condition() 49 | self._messageHandler = BoxRouterMessageHandler(self._weakRefRouter, self) 50 | super(AstroprintBoxRouterClient, self).__init__(hostname) 51 | 52 | def get_printer_listener(self): 53 | return self._printerListener 54 | 55 | def __del__(self): 56 | router = self._weakRefRouter() 57 | router.unregisterEvents() 58 | 59 | def send(self, data): 60 | with self._condition: 61 | if not self.terminated: 62 | try: 63 | super(AstroprintBoxRouterClient, self).send(data) 64 | 65 | except socket.error as e: 66 | self._logger.error('Error raised during send: %s' % e) 67 | 68 | self._error = True 69 | 70 | #Something happened to the link. Let's try to reset it 71 | self.close() 72 | 73 | def ponged(self, pong): 74 | if str(pong) == LINE_CHECK_STRING: 75 | self.outstandingPings -= 1 76 | 77 | def lineCheck(self, timeout=30): 78 | while not self.terminated: 79 | sleep(timeout) 80 | if self.terminated: 81 | break 82 | 83 | if self.outstandingPings > 0: 84 | self._logger.error('The line seems to be down') 85 | 86 | router = self._weakRefRouter() 87 | router.close() 88 | router._doRetry() 89 | break 90 | 91 | if time() - self._lastReceived > timeout: 92 | try: 93 | self.send(PingControlMessage(data=LINE_CHECK_STRING)) 94 | self.outstandingPings += 1 95 | 96 | except socket.error: 97 | self._logger.error("Line Check failed to send") 98 | 99 | #retry connection 100 | router = self._weakRefRouter() 101 | router.close() 102 | router._doRetry() 103 | 104 | self._lineCheckThread = None 105 | 106 | def terminate(self): 107 | #This is code to fix an apparent error in ws4py 108 | try: 109 | self._th = None #If this is not freed, the socket can't be freed because of circular references 110 | super(AstroprintBoxRouterClient, self).terminate() 111 | 112 | except AttributeError as e: 113 | if self.stream is None: 114 | self.environ = None 115 | else: 116 | raise e 117 | 118 | def opened(self): 119 | self.outstandingPings = 0 120 | self._lineCheckThread = threading.Thread(target=self.lineCheck) 121 | self._lineCheckThread.daemon = True 122 | self._error = False 123 | self._lineCheckThread.start() 124 | 125 | def closed(self, code, reason=None): 126 | #only retry if the connection was terminated by the remote or a link check failure (silentReconnect) 127 | router = self._weakRefRouter() 128 | 129 | if self._error or (self.server_terminated and router and router.connected): 130 | router.close() 131 | router._doRetry() 132 | 133 | def received_message(self, m): 134 | self._lastReceived = time() 135 | msg = json.loads(str(m)) 136 | method = getattr(self._messageHandler, msg['type'], None) 137 | if method: 138 | response = method(msg) 139 | if response is not None: 140 | self.send(json.dumps(response)) 141 | 142 | else: 143 | self._logger.warn('Unknown message type [%s] received' % msg['type']) 144 | 145 | 146 | class AstroprintBoxRouter(object): 147 | RETRY_SCHEDULE = [2, 2, 4, 10, 20, 30, 60, 120, 240, 480, 3600, 10800, 28800, 43200, 86400, 86400] #seconds to wait before retrying. When all exahusted it gives up 148 | 149 | STATUS_DISCONNECTED = 'disconnected' 150 | STATUS_CONNECTING = 'connecting' 151 | STATUS_CONNECTED = 'connected' 152 | STATUS_ERROR = 'error' 153 | 154 | def __init__(self, plugin): 155 | self._pendingClientRequests = {} 156 | self._retries = 0 157 | self._retryTimer = None 158 | self.ws = None 159 | self._silentReconnect = False 160 | self.status = self.STATUS_DISCONNECTED 161 | self.connected = False 162 | self.authenticated = False 163 | self.plugin = plugin 164 | self.watcherRegistered = False 165 | self._printerListener = None 166 | self._eventSender = None 167 | self._settings = self.plugin.get_settings() 168 | self._logger = self.plugin.get_logger() 169 | self._address = self._settings.get(["webSocket"]) 170 | 171 | 172 | def shutdown(self): 173 | self._logger.info('Shutting down Box router...') 174 | 175 | if self._retryTimer: 176 | self._retryTimer.cancel() 177 | self._retryTimer = None 178 | 179 | self._pendingClientRequests = None 180 | self.boxrouter_disconnect() 181 | 182 | #make sure we destroy the singleton 183 | global _instance 184 | _instance = None 185 | 186 | def boxrouter_connect(self): 187 | if not self.connected: 188 | if self.plugin.user: 189 | self._logger.info("Connecting to Box Router as [%s - %s]" % (self._settings.get(["boxName"]), self.plugin.boxId)) 190 | self._publicKey = self.plugin.user['id'] 191 | self._privateKey = self.plugin.user['accessKey'] 192 | accessKey = self.plugin.astroprintCloud.getToken() 193 | ##if self._publicKey and self._privateKey: 194 | if accessKey: 195 | self.status = self.STATUS_CONNECTING 196 | self.plugin.send_event("boxrouterStatus", self.STATUS_CONNECTING) 197 | 198 | try: 199 | if self._retryTimer: 200 | #This is in case the user tried to connect and there was a pending retry 201 | self._retryTimer.cancel() 202 | self._retryTimer = None 203 | #If it fails, the retry sequence should restart 204 | self._retries = 0 205 | 206 | if self.ws and not self.ws.terminated: 207 | self.ws.terminate() 208 | 209 | self.ws = AstroprintBoxRouterClient(self._address, self, self.plugin) 210 | self.ws.connect() 211 | self.connected = True 212 | if not self._printerListener: 213 | self._printerListener = self.plugin.get_printer_listener() 214 | self._printerListener.addWatcher(self) 215 | 216 | except Exception as e: 217 | self._logger.error("Error connecting to boxrouter: %s" % e) 218 | self.connected = False 219 | self.status = self.STATUS_ERROR 220 | self.plugin.send_event("boxrouterStatus", self.STATUS_ERROR) 221 | 222 | if self.ws: 223 | self.ws.terminate() 224 | self.ws = None 225 | 226 | self._doRetry(False) #This one should not be silent 227 | 228 | return True 229 | 230 | return False 231 | 232 | def boxrouter_disconnect(self): 233 | self.close() 234 | 235 | def close(self): 236 | if self.connected: 237 | self.authenticated = False 238 | self.connected = False 239 | 240 | self._publicKey = None 241 | self._privateKey = None 242 | self.status = self.STATUS_DISCONNECTED 243 | self.plugin.send_event("boxrouterStatus", self.STATUS_DISCONNECTED) 244 | 245 | self._printerListener.removeWatcher() 246 | 247 | if self.ws: 248 | self.unregisterEvents() 249 | if not self.ws.terminated: 250 | self.ws.terminate() 251 | 252 | self.ws = None 253 | 254 | def _doRetry(self, silent=True): 255 | if self._retries < len(self.RETRY_SCHEDULE): 256 | def retry(): 257 | self._retries += 1 258 | self._logger.info('Retrying boxrouter connection. Retry #%d' % self._retries) 259 | self._silentReconnect = silent 260 | self._retryTimer = None 261 | self.boxrouter_connect() 262 | 263 | if not self._retryTimer: 264 | self._logger.info('Waiting %d secs before retrying...' % self.RETRY_SCHEDULE[self._retries]) 265 | self._retryTimer = threading.Timer(self.RETRY_SCHEDULE[self._retries] , retry ) 266 | self._retryTimer.start() 267 | 268 | else: 269 | self._logger.info('No more retries. Giving up...') 270 | self.status = self.STATUS_DISCONNECTED 271 | self.plugin.send_event("boxrouterStatus", self.STATUS_DISCONNECTED) 272 | self._retries = 0 273 | self._retryTimer = None 274 | 275 | 276 | def cancelRetry(self): 277 | if self._retryTimer: 278 | self._retryTimer.cancel() 279 | self._retryTimer = None 280 | 281 | def completeClientRequest(self, reqId, data): 282 | if reqId in self._pendingClientRequests: 283 | req = self._pendingClientRequests[reqId] 284 | del self._pendingClientRequests[reqId] 285 | 286 | if req["callback"]: 287 | args = req["args"] or [] 288 | req["callback"](*([data] + args)) 289 | 290 | else: 291 | self._logger.warn('Attempting to deliver a client response for a request[%s] that\'s no longer pending' % reqId) 292 | 293 | def sendRequestToClient(self, clientId, type, data, timeout, respCallback, args=None): 294 | reqId = uuid.uuid4().hex 295 | 296 | if self.send({ 297 | 'type': 'request_to_client', 298 | 'data': { 299 | 'clientId': clientId, 300 | 'timeout': timeout, 301 | 'reqId': reqId, 302 | 'type': type, 303 | 'payload': data 304 | } 305 | }): 306 | self._pendingClientRequests[reqId] = { 307 | 'callback': respCallback, 308 | 'args': args, 309 | 'timeout': timeout 310 | } 311 | 312 | def sendEventToClient(self, clientId, type, data): 313 | self.send({ 314 | 'type': 'send_event_to_client', 315 | 'data': { 316 | 'clientId': clientId, 317 | 'eventType': type, 318 | 'eventData': data 319 | } 320 | }) 321 | 322 | def send(self, data): 323 | if self.ws and self.connected: 324 | self.ws.send(json.dumps(data)) 325 | return True 326 | 327 | else: 328 | self._logger.error('Unable to send data: Socket not active') 329 | return False 330 | 331 | def sendEvent(self, event, data): 332 | if self.watcherRegistered: 333 | dataToSend = ({ 334 | 'type': 'send_event', 335 | 'data': { 336 | 'eventType': event, 337 | 'eventData': data 338 | } 339 | }) 340 | return self.send(dataToSend) 341 | else: 342 | return True 343 | 344 | def registerEvents(self): 345 | if not self._printerListener: 346 | self._printerListener = self.plugin.get_printer_listener() 347 | if not self._eventSender: 348 | self._eventSender = EventSender(self) 349 | self._eventSender.connect() 350 | self.watcherRegistered = True 351 | 352 | def unregisterEvents(self): 353 | self.watcherRegistered = False 354 | 355 | def broadcastEvent(self, event, data): 356 | if self._eventSender: 357 | self._eventSender.sendUpdate(event, data) 358 | 359 | def triggerEvent(self, event, data): 360 | if self._eventSender: 361 | method = getattr(self._eventSender, event, None) 362 | if method: 363 | method(data) 364 | else: 365 | self._logger.warn('Unknown event type [%s] received' % event) 366 | 367 | 368 | def processAuthenticate(self, data): 369 | if data: 370 | self._silentReconnect = False 371 | if 'error' in data and data['error']: 372 | self._logger.warn("Box Router Authentication Error: %s" % data['message'] if 'message' in data else 'Unkonwn authentication error') 373 | self.status = self.STATUS_ERROR 374 | self.plugin.send_event("boxrouterStatus", self.STATUS_ERROR) 375 | self.close() 376 | errorType = data['type'] if 'type' in data else None 377 | 378 | if errorType == 'box_id_in_use': 379 | self._logger.warn("Box Router is reporting that the box id [%s] is in use by another box. Is this image a clone of another? consider deleting the file at %s" % (self.plugin.boxId, os.path.join(os.path.dirname(self._settings._configfile), "box-id"))) 380 | elif 'should_retry' in data and data['should_retry']: 381 | self._doRetry() 382 | elif errorType == 'unable_to_authenticate': 383 | self._logger.info("Box Router unable to authenticate user. No retries, logging out") 384 | self.plugin.astroprintCloud.unauthorizedHandler() 385 | 386 | elif 'success' in data and data['success']: 387 | self._logger.info("Box Router connected to astroprint service") 388 | if 'groupId' in data: 389 | self.plugin.astroprintCloud.updateFleetInfo(data['orgId'], data['groupId']) 390 | self.authenticated = True 391 | self._retries = 0 392 | self._retryTimer = None 393 | self.status = self.STATUS_CONNECTED 394 | self.plugin.send_event("boxrouterStatus", self.STATUS_CONNECTED) 395 | self.plugin.astroprintCloud.sendCurrentData() 396 | 397 | return None 398 | 399 | else: 400 | boxName = self._settings.get(["boxName"]) 401 | platform = sys.platform 402 | localIpAddress = octoprint.util.address_for_client("google.com", 80) 403 | mayor, minor, build = self.plugin.get_plugin_version().split(".") 404 | return { 405 | 'type': 'auth', 406 | 'data': { 407 | 'silentReconnect': self._silentReconnect, 408 | 'boxId': self.plugin.boxId, 409 | 'variantId': self._settings.get(["product_variant_id"]), 410 | 'boxName': boxName, 411 | 'swVersion': "OctoPrint Plugin - v%s.%s(%s)" % (mayor, minor, build), 412 | 'platform': platform, 413 | 'localIpAddress': localIpAddress, 414 | 'accessToken' : self.plugin.astroprintCloud.getToken(), 415 | #'publicKey': self._publicKey, 416 | #'privateKey': self._privateKey, 417 | 'printerModel': self._settings.get(["printerModel"]) if self._settings.get(['printerModel'])['id'] else None 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /octoprint_astroprint/boxrouter/events.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import json 9 | 10 | from copy import deepcopy 11 | 12 | class EventSender(object): 13 | def __init__(self, socket): 14 | self._socket = socket 15 | self._logger = socket.plugin.get_logger() 16 | 17 | def connect(self): 18 | self._lastSent = { 19 | 'temp_update': None, 20 | 'status_update': None, 21 | 'printing_progress': None, 22 | 'print_capture': None, 23 | 'print_file_download': None, 24 | 'filament_update' : None, 25 | } 26 | 27 | 28 | def onCaptureInfoChanged(self, payload): 29 | self.sendUpdate('print_capture', payload) 30 | 31 | def filamentChanged(self, payload): 32 | self.sendUpdate('filament_update', payload) 33 | 34 | def onDownload(self, payload): 35 | data = { 36 | 'id': payload['id'], 37 | 'selected': False 38 | } 39 | 40 | if payload['type'] == 'error': 41 | data['error'] = True 42 | data['message'] = payload['reason'] if 'reason' in payload else 'Problem downloading' 43 | 44 | elif payload['type'] == 'cancelled': 45 | data['cancelled'] = True 46 | 47 | else: 48 | data['progress'] = 100 if payload['type'] == 'success' else payload['progress'] 49 | 50 | self.sendUpdate('print_file_download', data) 51 | 52 | def onDownloadComplete(self, data): 53 | if data['isBeingPrinted']: 54 | payload = { 55 | 'id': data['id'], 56 | 'progress': 100, 57 | 'selected': True 58 | } 59 | if data['printjob_id']: 60 | payload['printjob_id'] = data['printjob_id'] 61 | else : 62 | payload = { 63 | 'id': data['id'], 64 | 'progress': 100, 65 | 'error': True, 66 | 'message': 'Unable to start printing', 67 | 'selected': False 68 | } 69 | self.sendUpdate('print_file_download', payload) 70 | 71 | 72 | 73 | def sendLastUpdate(self, event): 74 | if event in self._lastSent: 75 | self._send(event, self._lastSent[event]) 76 | 77 | def sendUpdate(self, event, data): 78 | if self._lastSent[event] != data and self._send(event, data): 79 | self._lastSent[event] = deepcopy(data) if data else None 80 | 81 | def _send(self, event, data): 82 | try: 83 | self._socket.sendEvent(event, data) 84 | return True 85 | 86 | except Exception as e: 87 | self._logger.error( 'Error sending [%s] event: %s' % (event, e) ) 88 | return False 89 | -------------------------------------------------------------------------------- /octoprint_astroprint/boxrouter/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import weakref 9 | import json 10 | 11 | from .requesthandler import RequestHandler 12 | 13 | class BoxRouterMessageHandler(object): 14 | def __init__(self, weakRefBoxRouter, wsClient): 15 | self._weakRefBoxRouter = weakRefBoxRouter 16 | self._weakWs = weakref.ref(wsClient) 17 | self._logger = wsClient.plugin.get_logger() 18 | self._printer = wsClient.plugin.get_printer() 19 | self._handler = None 20 | self._subscribers = 0 21 | 22 | def auth(self, msg): 23 | router = self._weakRefBoxRouter() 24 | if router: 25 | return router.processAuthenticate((msg['data'] if 'data' in msg else None)) 26 | else: 27 | return None 28 | 29 | def set_temp(self, msg): 30 | payload = {} 31 | 32 | if self._printer.is_operational(): 33 | payload = msg['payload'] 34 | self._printer.set_temperature(payload['target'] or 0.0, payload['value'] or 0.0) 35 | return None 36 | 37 | def update_subscribers(self, msg): 38 | wsClient = self._weakWs() 39 | 40 | if wsClient: 41 | self._subscribers += int(msg['data']) 42 | router = self._weakRefBoxRouter() 43 | if self._subscribers > 0: 44 | router.registerEvents() 45 | else: 46 | self._subscribers = 0 47 | router.unregisterEvents() 48 | 49 | return None 50 | 51 | def force_event(self, msg): 52 | router = self._weakRefBoxRouter() 53 | 54 | if router: 55 | router.triggerEvent('sendLastUpdate', msg['data']) 56 | 57 | return None 58 | 59 | def request(self, msg): 60 | 61 | wsClient = self._weakWs() 62 | 63 | if wsClient: 64 | if not self._handler: 65 | self._handler = RequestHandler(wsClient) 66 | response = None 67 | 68 | try: 69 | request = msg['data']['type'] 70 | reqId = msg['reqId'] 71 | clientId = msg['clientId'] 72 | data = msg['data']['payload'] 73 | 74 | method = getattr(self._handler, request, None) 75 | if method: 76 | def sendResponse(result): 77 | if result is None: 78 | result = {'success': True} 79 | 80 | wsClient.send(json.dumps({ 81 | 'type': 'req_response', 82 | 'reqId': reqId, 83 | 'data': result 84 | })) 85 | 86 | method(data, clientId, sendResponse) 87 | 88 | else: 89 | response = { 90 | 'error': True, 91 | 'message': 'This Box does not recognize the request type [%s]' % request 92 | } 93 | 94 | except Exception as e: 95 | message = 'Error sending [%s] response: %s' % (request, e) 96 | self._logger.error( message , exc_info= True) 97 | response = {'error': True, 'message': message } 98 | 99 | if response: 100 | wsClient.send(json.dumps({ 101 | 'type': 'req_response', 102 | 'reqId': reqId, 103 | 'data': response 104 | })) 105 | 106 | #else: 107 | # this means that the handler is asynchronous 108 | # and will respond when done 109 | # we should probably have a timeout here too 110 | # even though there's already one at the boxrouter 111 | 112 | def response_from_client(self, msg): 113 | 114 | router = self._weakRefBoxRouter() 115 | 116 | if router: 117 | router.completeClientRequest(msg['reqId'], msg['data']) 118 | -------------------------------------------------------------------------------- /octoprint_astroprint/boxrouter/handlers/requesthandler.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import base64 9 | import threading 10 | import re 11 | 12 | from time import sleep 13 | 14 | class RequestHandler(object): 15 | def __init__(self, wsClient): 16 | 17 | self.wsClient = wsClient 18 | self.plugin = self.wsClient.plugin 19 | self.astroprintCloud = self.plugin.astroprintCloud 20 | self._printer = self.plugin.get_printer() 21 | self._printerListener = self.wsClient.get_printer_listener() 22 | self._logger = self.plugin.get_logger() 23 | self.cameraManager = self.plugin.cameraManager 24 | self._settings = self.plugin.get_settings() 25 | 26 | def initial_state(self, data, clientId, done): 27 | if not self.astroprintCloud: 28 | self.astroprintCloud = self.plugin.astroprintCloud 29 | dataProfile = self.wsClient.plugin._printer_profile_manager.get_current_or_default() 30 | 31 | profile = { 32 | 'driver': "marlin", #At the moment octopi only supports marlin 33 | 'extruder_count': dataProfile['extruder']['count'], 34 | 'max_nozzle_temp': self._settings.get(["max_nozzle_temp"]), 35 | 'max_bed_temp': self._settings.get(["max_bed_temp"]), 36 | 'heated_bed': dataProfile['heatedBed'], 37 | 'cancel_gcode': ['G28 X0 Y0'], 38 | 'invert_z': dataProfile['axes']['z']['inverted'], 39 | 'printer_model': self._settings.get(["printerModel"]) if self._settings.get(['printerModel'])['id'] else None, 40 | 'filament' : self._settings.get(["filament"]) 41 | } 42 | 43 | state = { 44 | 'printing': self._printer.is_printing() or self._printer.is_paused(), 45 | 'heatingUp': self.plugin.printerIsHeating(), 46 | 'operational': self._printer.is_operational(), 47 | 'ready_to_print': self.plugin.isBedClear and self._printer.is_operational() and not (self._printer.is_printing() or self._printer.is_paused()), 48 | 'paused': self._printer.is_paused(), 49 | 'camera': True, #self.cameraManager.cameraActive, 50 | 'filament' : self._settings.get(["filament"]), 51 | 'printCapture': self.cameraManager.timelapseInfo, 52 | 'profile': profile, 53 | 'capabilities': self.plugin.capabilities, 54 | 'tool' : self.plugin.currentTool() 55 | } 56 | 57 | if state['printing'] or state['paused']: 58 | #Let's add info about the ongoing print job 59 | current_job = self._printer.get_current_job() 60 | printFile = self.astroprintCloud.db.getPrintFileByOctoPrintPath(current_job['file']['path']) 61 | 62 | state['job'] = { 63 | "estimatedPrintTime": current_job['estimatedPrintTime'], 64 | "layerCount": self._printerListener.get_analyzed_job_layers()['layerCount'] if self._printerListener.get_analyzed_job_layers() else None , 65 | "file": { 66 | "origin": current_job['file']['origin'], 67 | "rendered_image": printFile.renderedImage if printFile else None, 68 | "name" : printFile.name if printFile else current_job['file']['name'], 69 | "cloudId" : printFile.printFileId if printFile else None, 70 | "date": current_job['file']['date'], 71 | "printFileName": printFile.printFileName if printFile else None, 72 | "size": current_job['file']['size'], 73 | }, 74 | "filament": current_job['filament'], 75 | } 76 | state['progress'] = self._printerListener.get_progress() 77 | 78 | self.astroprintCloud.sendCurrentData() 79 | 80 | done(state) 81 | 82 | def job_info(self, data, clientId, done): 83 | if self._printerListener.get_job_data() and not self._printerListener.get_job_data()['layerCount']: 84 | t = threading.Timer(0.5, self.job_info, [data, clientId, done]) 85 | t.start() 86 | else: 87 | done(self._printerListener.get_job_data()) 88 | 89 | def printerCommand(self, data, clientId, done): 90 | self._handleCommandGroup(PrinterCommandHandler, data, clientId, done, self.plugin) 91 | 92 | 93 | def printCapture(self, data, clientId, done): 94 | freq = data['freq'] 95 | if freq: 96 | cm = self.cameraManager 97 | 98 | if cm.timelapseInfo: 99 | if not cm.update_timelapse(freq): 100 | done({ 101 | 'error': True, 102 | 'message': 'Error updating the print capture' 103 | }) 104 | return 105 | 106 | else: 107 | r = cm.start_timelapse(freq) 108 | if r != 'success': 109 | done({ 110 | 'error': True, 111 | 'message': 'Error creating the print capture: %s' % r 112 | }) 113 | return 114 | else: 115 | done({ 116 | 'error': True, 117 | 'message': 'Frequency required' 118 | }) 119 | return 120 | 121 | done(None) 122 | 123 | def signoff(self, data, clientId, done): 124 | self._logger.info('Remote signoff requested.') 125 | threading.Timer(1, self.astroprintCloud.unauthorizedHandler, [False]).start() 126 | done(None) 127 | 128 | def notifyfleet(self, data, clientId, done): 129 | self._logger.info("Box has been joined to a fleet group") 130 | self.astroprintCloud.getFleetInfo() 131 | done(None) 132 | 133 | def print_file(self, data, clientId, done): 134 | print_file_id = data['printFileId'] 135 | 136 | if 'printJobId' in data and data['printJobId']: 137 | print_job_data = {'print_job_id' : data['printJobId'], 'print_file' : print_file_id} 138 | else : 139 | print_job_data = None 140 | 141 | if self.plugin.isBedClear : 142 | state = { 143 | "type": "progress", 144 | "id": print_file_id, 145 | "progress": 0 146 | } 147 | done(state) 148 | self.astroprintCloud.printFile(print_file_id, print_job_data, True) 149 | else: 150 | state = { 151 | 'id': print_file_id, 152 | 'progress': 100, 153 | 'error': True, 154 | 'message': 'Unable to start printing', 155 | 'selected': False 156 | } 157 | done(state) 158 | 159 | def cancel_download(self, data, clientId, done): 160 | print_file_id = data['printFileId'] 161 | self.astroprintCloud.cancelDownload(print_file_id) 162 | 163 | done(None) 164 | 165 | def set_filament(self, data, clientId, done): 166 | 167 | filament = {} 168 | 169 | if data['filament'] and data['filament']['name'] and data['filament']['color']: 170 | filament['name'] = data['filament']['name'] 171 | #Better to make sure that are getting right color codes 172 | if re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', data['filament']['color']): 173 | filament['color'] = data['filament']['color'] 174 | self._settings.set(['filament'], filament) 175 | self._settings.save() 176 | self.astroprintCloud.bm.triggerEvent('filamentChanged', data) 177 | done(None) 178 | else: 179 | done({ 180 | 'error': True, 181 | 'message': 'Invalid color code' 182 | }) 183 | 184 | else: 185 | data['filament'] = None 186 | self._settings.set(['filament'], None) 187 | self._settings.save() 188 | self.astroprintCloud.bm.triggerEvent('filamentChanged', data) 189 | done(None) 190 | 191 | #set CommandGroup for future camera and 2p2 updates 192 | def _handleCommandGroup(self, handlerClass, data, clientId, done, plugin = None): 193 | handler = handlerClass(plugin) 194 | 195 | command = data['command'] 196 | options = data['options'] 197 | 198 | method = getattr(handler, command, None) 199 | if method: 200 | method(options, clientId, done) 201 | 202 | else: 203 | done({ 204 | 'error': True, 205 | 'message': '%s::%s is not supported' % (handlerClass, command) 206 | }) 207 | 208 | 209 | # Printer Command Group Handler 210 | class PrinterCommandHandler(object): 211 | 212 | def __init__(self, plugin): 213 | self.plugin = plugin 214 | self._printer = self.plugin.get_printer() 215 | self._settings = self.plugin.get_settings() 216 | self._logger = self.plugin.get_logger() 217 | self.cameraManager = self.plugin.cameraManager 218 | 219 | def pause(self, data, clientId, done): 220 | self._printer.pause_print() 221 | done(None) 222 | 223 | def resume(self, data, clientId, done): 224 | self._printer.resume_print() 225 | done(None) 226 | 227 | def cancel(self, data, clientId, done): 228 | data = {'print_job_id': self.plugin.astroprintCloud.currentPrintingJob} 229 | self._printer.cancel_print() 230 | done(None) 231 | 232 | def photo(self, data, clientId, done): 233 | pic = self.cameraManager.getPic() 234 | 235 | if pic is not None: 236 | done({ 237 | 'success': True, 238 | 'image_data': base64.b64encode(pic).decode() 239 | }) 240 | else: 241 | done({ 242 | 'success': False, 243 | 'image_data': '' 244 | }) 245 | 246 | def set_bed_clear(self, clear, _, done): 247 | if self._settings.get(['check_clear_bed']): 248 | self.plugin.set_bed_clear(clear) 249 | done(None) 250 | -------------------------------------------------------------------------------- /octoprint_astroprint/cameramanager/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import time 9 | import requests 10 | import traceback 11 | import warnings 12 | 13 | ## Python2/3 compatibile import 14 | try: 15 | from StringIO import StringIO 16 | except ImportError: 17 | from io import BytesIO as StringIO 18 | 19 | from threading import Event 20 | 21 | 22 | try: 23 | from PIL import Image 24 | except ImportError: 25 | Image = None 26 | import subprocess 27 | traceback.print_exc() 28 | warnings.warn("PIL/Pillow is not available. Will fallback to " 29 | + "ImageMagick, make sure it is installed.") 30 | 31 | # singleton 32 | _instance = None 33 | 34 | def cameraManager(plugin): 35 | global _instance 36 | if _instance is None: 37 | _instance = CameraManager(plugin) 38 | return _instance 39 | 40 | import threading 41 | from threading import Thread 42 | import os.path 43 | import time 44 | import logging 45 | 46 | from sys import platform 47 | 48 | # 49 | # Camera Manager base class 50 | # 51 | 52 | class CameraManager(object): 53 | def __init__(self, plugin): 54 | self.name = None 55 | self.cameraActive = False 56 | self.astroprintCloud = None #set up when astroprint cloud is initialized 57 | 58 | #RECTIFYNIG default settings 59 | self.plugin = plugin 60 | self._settings = self.plugin.get_settings() 61 | self._logger = self.plugin.get_logger() 62 | self._printer = self.plugin.get_printer() 63 | self.checkCameraStatus() 64 | self._image_transpose = (self._settings.global_get(["webcam", "flipH"]) or 65 | self._settings.global_get(["webcam", "flipV"]) or 66 | self._settings.global_get(["webcam", "rotate90"])) 67 | self._photos = {} # To hold sync photos 68 | self.timelapseWorker = None 69 | self.timelapseInfo = None 70 | self.plugin.get_printer_listener().cameraManager = self 71 | 72 | def layerChanged(self): 73 | if self.timelapseInfo and self.timelapseInfo['freq'] == "layer": 74 | self.addPhotoToTimelapse(self.timelapseInfo['id']) 75 | 76 | def checkCameraStatus(self): 77 | snapshotUrl = self._settings.global_get(["webcam", "snapshot"]) 78 | camUrl = self._settings.global_get(["webcam", "stream"]) 79 | if snapshotUrl and camUrl: 80 | try: 81 | r = requests.get( 82 | snapshotUrl 83 | ) 84 | if r.status_code == 200: 85 | camera = True 86 | else : 87 | camera = False 88 | 89 | except Exception as e: 90 | self._logger.error("Error getting camera status: %s" % e) 91 | camera = False 92 | 93 | else: 94 | camera = False 95 | if camera != self.cameraActive: 96 | self.cameraActive = camera 97 | self.plugin.send_event("cameraStatus", self.cameraActive) 98 | self.plugin.sendSocketInfo() 99 | self._settings.set(['camera'], self.cameraActive) 100 | 101 | def cameraError(self): 102 | if self.cameraActive: 103 | self.cameraActive = False 104 | self._settings.set(['camera'], self.cameraActive) 105 | self.plugin.send_event("cameraStatus", self.cameraActive) 106 | if self.astroprintCloud: 107 | self.astroprintCloud.sendCurrentData() 108 | 109 | def printStarted(self): 110 | self.timelapseInfo = None 111 | 112 | def cameraConnected(self): 113 | if not self.cameraActive: 114 | self.cameraActive = True 115 | self._settings.set(['camera'], self.cameraActive) 116 | self.plugin.send_event("cameraStatus", self.cameraActive) 117 | if self.astroprintCloud: 118 | self.astroprintCloud.sendCurrentData() 119 | 120 | def shutdown(self): 121 | self._logger.info('Shutting down Camera Manager...') 122 | if self.timelapseWorker: 123 | self.timelapseWorker.stop() 124 | self.timelapseWorker = None 125 | 126 | global _instance 127 | _instance = None 128 | 129 | def getPic(self): 130 | if not self.cameraActive: 131 | return None 132 | else: 133 | snapshotUrl = self._settings.global_get(["webcam", "snapshot"]) 134 | 135 | try: 136 | r = requests.get(snapshotUrl) 137 | pic = r.content 138 | if pic is not None: 139 | if self._settings.global_get(["webcam", "flipH"]) or self._settings.global_get(["webcam", "flipV"]) or self._settings.global_get(["webcam", "rotate90"]): 140 | if Image: 141 | buf = StringIO() 142 | buf.write(pic) 143 | image = Image.open(buf) 144 | if self._settings.global_get(["webcam", "flipH"]): 145 | image = image.transpose(Image.FLIP_LEFT_RIGHT) 146 | if self._settings.global_get(["webcam", "flipV"]): 147 | image = image.transpose(Image.FLIP_TOP_BOTTOM) 148 | if self._settings.global_get(["webcam", "rotate90"]): 149 | image = image.transpose(Image.ROTATE_90) 150 | transformedImage = StringIO() 151 | image.save(transformedImage, format="jpeg") 152 | transformedImage.seek(0, 2) 153 | transformedImage.seek(0) 154 | pic = transformedImage.read() 155 | else: 156 | args = ["convert", "-"] 157 | if self._settings.global_get(["webcam", "flipV"]): 158 | args += ["-flip"] 159 | if self._settings.global_get(["webcam", "flipH"]): 160 | args += ["-flop"] 161 | if self._settings.global_get(["webcam", "rotate90"]): 162 | args += ["-rotate", "90"] 163 | args += "jpeg:-" 164 | p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 165 | pic, _ = p.communicate(pic) 166 | 167 | if not self.cameraActive: 168 | self.cameraConnected() 169 | return pic 170 | except Exception as e: 171 | self._logger.exception("Error getting pic: %s" % e) 172 | self.cameraError() 173 | return None 174 | 175 | 176 | 177 | def addPhotoToTimelapse(self, timelapseId, waitForPhoto = False): 178 | #Build text 179 | ''' 180 | printerData = self.plugin.get_printer_listener().get_progress() 181 | text = "%d%% - Layer %s%s" % ( 182 | printerData['progress']['completion'], 183 | str(printerData['progress']['currentLayer']) if printerData['progress']['currentLayer'] else '--', 184 | "/%s" % str(printerData['job']['layerCount'] if printerData['job']['layerCount'] else '') 185 | ) 186 | ''' 187 | picBuf = self.getPic() 188 | 189 | if picBuf: 190 | picData = self.astroprintCloud.uploadImageFile(timelapseId, picBuf) 191 | #we need to check again as it's possible that this was the last 192 | #pic and the timelapse is closed. 193 | if picData and self.timelapseInfo: 194 | self.timelapseInfo['last_photo'] = picData['url'] 195 | #Here we send the box confirmation: 196 | self.astroprintCloud.bm.triggerEvent('onCaptureInfoChanged', self.timelapseInfo) 197 | if waitForPhoto: 198 | return True 199 | 200 | #self.get_pic_async(onDone, text) 201 | 202 | 203 | def start_timelapse(self, freq): 204 | if not self.cameraActive: 205 | return 'no_camera' 206 | 207 | if freq == '0': 208 | return 'invalid_frequency' 209 | 210 | if self.timelapseWorker: 211 | self.stop_timelapse() 212 | #check that there's a print ongoing otherwise don't start 213 | current_job = self._printer.get_current_job() 214 | if not current_job: 215 | return 'no_print_file_selected' 216 | printCapture = self.astroprintCloud.startPrintCapture(current_job['file']['name'], current_job['file']['path']) 217 | if printCapture['error']: 218 | return printCapture['error'] 219 | 220 | else: 221 | self.timelapseInfo = { 222 | 'id': printCapture['print_id'], 223 | 'freq': freq, 224 | 'paused': False, 225 | 'last_photo': None 226 | } 227 | 228 | if freq == 'layer': 229 | # send first pic and subscribe to layer change events 230 | self.addPhotoToTimelapse(printCapture['print_id']) 231 | 232 | else: 233 | 234 | try: 235 | freq = float(freq) 236 | except ValueError: 237 | self._logger.info("invalid_frequency") 238 | return 'invalid_frequency' 239 | 240 | self.timelapseInfo['freq'] = freq 241 | self.timelapseWorker = TimelapseWorker(self, printCapture['print_id'], freq) 242 | self.timelapseWorker.start() 243 | 244 | return 'success' 245 | 246 | return 'unkonwn_error' 247 | 248 | def update_timelapse(self, freq): 249 | if self.timelapseInfo and self.timelapseInfo['freq'] != freq: 250 | if freq == 'layer': 251 | if self.timelapseWorker and not self.timelapseWorker.isPaused(): 252 | self.pause_timelapse() 253 | 254 | # subscribe to layer change events 255 | else: 256 | try: 257 | freq = float(freq) 258 | except ValueError as e: 259 | self._logger.error("Error updating timelapse: %s" % e) 260 | return False 261 | 262 | # if subscribed to layer change events, unsubscribe here 263 | 264 | if freq == 0: 265 | self.pause_timelapse() 266 | elif not self.timelapseWorker: 267 | self.timelapseWorker = TimelapseWorker(self, self.timelapseInfo['id'], freq) 268 | self.timelapseWorker.start() 269 | elif self.timelapseWorker.isPaused(): 270 | self.timelapseWorker.timelapseFreq = freq 271 | self.resume_timelapse() 272 | else: 273 | self.timelapseWorker.timelapseFreq = freq 274 | 275 | self.timelapseInfo['freq'] = freq 276 | 277 | return True 278 | 279 | return False 280 | 281 | def stop_timelapse(self, takeLastPhoto = False): 282 | 283 | if self.timelapseWorker: 284 | self.timelapseWorker.stop() 285 | self.timelapseWorker = None 286 | 287 | if takeLastPhoto and self.timelapseInfo: 288 | self.addPhotoToTimelapse(self.timelapseInfo['id']) 289 | 290 | self.timelapseInfo = None 291 | self.astroprintCloud.bm.triggerEvent('onCaptureInfoChanged', self.timelapseInfo) 292 | 293 | return True 294 | 295 | def pause_timelapse(self): 296 | if self.timelapseWorker: 297 | if not self.timelapseWorker.isPaused(): 298 | self.timelapseWorker.pause() 299 | self.timelapseInfo['paused'] = True 300 | self.astroprintCloud.bm.triggerEvent('onCaptureInfoChanged', self.timelapseInfo) 301 | return True 302 | 303 | return False 304 | 305 | def resume_timelapse(self): 306 | if self.timelapseWorker: 307 | if self.timelapseWorker.isPaused(): 308 | self.timelapseWorker.resume() 309 | self.timelapseInfo['paused'] = False 310 | self.astroprintCloud.bm.triggerEvent('onCaptureInfoChanged', self.timelapseInfo) 311 | return True 312 | 313 | return False 314 | 315 | def is_timelapse_active(self): 316 | return self.timelapseWorker is not None 317 | 318 | @property 319 | def capabilities(self): 320 | return [] 321 | 322 | # 323 | # Thread to take timed timelapse pictures 324 | # 325 | 326 | class TimelapseWorker(threading.Thread): 327 | def __init__(self, manager, timelapseId, timelapseFreq): 328 | super(TimelapseWorker, self).__init__() 329 | 330 | self._stopExecution = False 331 | self._cm = manager 332 | self._resumeFromPause = threading.Event() 333 | 334 | self.daemon = True 335 | self.timelapseId = timelapseId 336 | self.timelapseFreq = timelapseFreq 337 | self._logger = manager._logger 338 | 339 | def run(self): 340 | lastUpload = 0 341 | self._resumeFromPause.set() 342 | while not self._stopExecution: 343 | if (time.time() - lastUpload) >= self.timelapseFreq and self._cm.addPhotoToTimelapse(self.timelapseId, True): 344 | lastUpload = time.time() 345 | 346 | time.sleep(1) 347 | self._resumeFromPause.wait() 348 | 349 | def stop(self): 350 | self._stopExecution = True 351 | if self.isPaused(): 352 | self.resume() 353 | 354 | self.join() 355 | 356 | def pause(self): 357 | self._resumeFromPause.clear() 358 | 359 | def resume(self): 360 | self._resumeFromPause.set() 361 | 362 | def isPaused(self): 363 | return not self._resumeFromPause.isSet() 364 | -------------------------------------------------------------------------------- /octoprint_astroprint/downloadmanager/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import requests 9 | import threading 10 | import os 11 | 12 | ## Python2/3 compatibile import 13 | try: 14 | from Queue import Queue 15 | except ImportError: 16 | from queue import Queue 17 | 18 | from flask import request 19 | 20 | from octoprint_astroprint.AstroprintDB import AstroprintPrintFile 21 | 22 | class DownloadWorker(threading.Thread): 23 | 24 | def __init__(self, manager): 25 | self._daemon = True 26 | self._manager = manager 27 | self._activeRequest = None 28 | self._canceled = False 29 | self.activeDownload = False 30 | self.plugin = manager.plugin 31 | self._logger = manager.plugin.get_logger() 32 | self.astroprintCloud = manager.astroprintCloud 33 | self.db = manager.astroprintCloud.db 34 | self.bm = manager.astroprintCloud.bm 35 | super(DownloadWorker, self).__init__() 36 | 37 | def run(self): 38 | downloadQueue = self._manager.queue 39 | 40 | while True: 41 | 42 | item = downloadQueue.get() 43 | if item == 'shutdown': 44 | return 45 | 46 | id = item['id'] 47 | name = item['name'] 48 | printNow = item['printNow'] 49 | fileName = name 50 | url = image = item['download_url'] 51 | destination = None 52 | printFile = False 53 | if not 'designDownload' in item: 54 | printFile = True 55 | if printFile: 56 | 57 | fileName = item['filename'] 58 | substr = ".gcode" 59 | idx = fileName.index(substr) 60 | fileName = fileName[:idx] + "-" + id[:7] + fileName[idx:] 61 | image = item['design']['images']['square'] if item['design'] else None 62 | 63 | 64 | self.activeDownload = id 65 | 66 | self._logger.info("Downloading %s" % fileName) 67 | 68 | try: 69 | 70 | r = requests.get( 71 | url, 72 | stream=True, 73 | timeout= (10.0, 60.0) 74 | ) 75 | 76 | if r.status_code == 200: 77 | content_length = float(r.headers.get('content-length')) 78 | downloaded_size = 0.0 79 | destination = "%s/%s" %(self.plugin._basefolder, fileName) 80 | with open(destination, 'wb') as file: 81 | for chunk in r.iter_content(100000): #download 100kb at a time 82 | downloaded_size += len(chunk) 83 | file.write(chunk) 84 | progress = 2 + round((downloaded_size / content_length) * 98.0, 1) 85 | if printFile: 86 | payload = { 87 | "id" : id, 88 | "progress" : progress, 89 | "type" : "progress", 90 | } 91 | self.bm.triggerEvent('onDownload', payload) 92 | self.plugin.send_event("download", {'id' : id, 'name': fileName, 'progress' : progress}) 93 | 94 | if self._canceled: #check again before going to read next chunk 95 | break 96 | 97 | r.raise_for_status() 98 | 99 | if self._canceled: 100 | self._manager._logger.warn('Download canceled for %s' % id) 101 | self.clearFile(destination) 102 | self.downloadCanceled(id, fileName) 103 | else: 104 | if printFile: 105 | pf = AstroprintPrintFile(id, name, fileName, fileName, image) 106 | self.astroprintCloud.wrapAndSave("printFile", pf, printNow) 107 | else: 108 | self.astroprintCloud.wrapAndSave("design", name, False) 109 | 110 | except requests.exceptions.HTTPError as err: 111 | self._logger.error(err) 112 | if printFile: 113 | payload = { 114 | "type" : "error", 115 | "reason" : err.response.text 116 | } 117 | if self.bm.watcherRegistered: 118 | self.bm.triggerEvent('onDownload', payload) 119 | self.plugin.send_event("download", {'id' : id, 'name': fileName, 'failed' : err.response.text }) 120 | self.clearFile(destination) 121 | return None 122 | except requests.exceptions.RequestException as e: 123 | self._logger.error(e) 124 | if printFile: 125 | payload = { 126 | "type" : "error", 127 | } 128 | if self.bm.watcherRegistered: 129 | self.bm.triggerEvent('onDownload', payload) 130 | self.plugin.send_event("download", {'id' : id, 'name': fileName, 'failed' : "Server Error"}) 131 | self.clearFile(destination) 132 | return None 133 | 134 | self.activeDownload = False 135 | self._canceled = False 136 | self._activeRequest = None 137 | downloadQueue.task_done() 138 | 139 | def cancel(self): 140 | if self.activeDownload: 141 | if self._activeRequest: 142 | self._activeRequest.close() 143 | 144 | self._manager._logger.warn('Download canceled requested for %s' % self.activeDownload) 145 | self._canceled = True 146 | 147 | def downloadCanceled(self, id, fileName): 148 | if fileName: 149 | payload = { 150 | "type" : "cancelled", 151 | "id" : id 152 | } 153 | self.bm.triggerEvent('onDownload', payload) 154 | self.plugin.send_event("download", {'id' : id, 'name': fileName, 'canceled' : True}) 155 | self._canceled = False 156 | 157 | def clearFile(self, destination): 158 | if destination and os.path.exists(destination): 159 | os.remove(destination) 160 | 161 | class DownloadManager(object): 162 | _maxWorkers = 3 163 | 164 | def __init__(self, astroprintCloud): 165 | self.astroprintCloud = astroprintCloud 166 | self.plugin = astroprintCloud.plugin 167 | self.queue = Queue() 168 | self._workers = [] 169 | self._logger = self.plugin.get_logger() 170 | for i in range(self._maxWorkers): 171 | w = DownloadWorker(self) 172 | w.daemon = True 173 | self._workers.append( w ) 174 | w.start() 175 | 176 | def isDownloading(self, id): 177 | for w in self._workers: 178 | if w.activeDownload == id: 179 | return True 180 | 181 | return False 182 | 183 | def startDownload(self, item): 184 | self.queue.put(item) 185 | 186 | def cancelDownload(self, id): 187 | for w in self._workers: 188 | if w.activeDownload == id: 189 | w.cancel() 190 | return True 191 | 192 | return False 193 | 194 | def shutdown(self): 195 | self._logger.info('Shutting down Download Manager...') 196 | for w in self._workers: 197 | self.queue.put('shutdown') 198 | if w.activeDownload: 199 | w.cancel() 200 | -------------------------------------------------------------------------------- /octoprint_astroprint/gCodeAnalyzer/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import json 9 | 10 | from threading import Thread 11 | from sarge import run, Capture 12 | 13 | class GCodeAnalyzer(Thread): 14 | 15 | def __init__(self,filename,layersInfo,readyCallback,exceptionCallback,parent, plugin): 16 | 17 | self._logger = plugin.get_logger() 18 | 19 | super(GCodeAnalyzer, self).__init__() 20 | 21 | self.plugin = plugin 22 | self.filename = filename 23 | 24 | self.readyCallback = readyCallback 25 | self.exceptionCallback = exceptionCallback 26 | self.layersInfo = layersInfo 27 | self.daemon = True 28 | 29 | self.layerList = None 30 | self.totalPrintTime = None 31 | self.layerCount = None 32 | self.size = None 33 | self.layerHeight = None 34 | self.totalFilament = None 35 | self.parent = parent 36 | 37 | def makeCalcs(self): 38 | self.start() 39 | 40 | def run(self): 41 | gcodeData = [] 42 | try: 43 | pipe = run( 44 | ('%s/util/AstroprintGCodeAnalyzer "%s" 1' if self.layersInfo else '%s/util/AstroprintGCodeAnalyzer "%s"') % ( 45 | self.plugin._basefolder, 46 | self.filename 47 | ), stdout=Capture()) 48 | 49 | if pipe.returncode == 0: 50 | try: 51 | gcodeData = json.loads(pipe.stdout.text) 52 | 53 | if self.layersInfo: 54 | self.layerList = gcodeData['layers'] 55 | 56 | self.totalPrintTime = gcodeData['print_time'] 57 | 58 | self.layerCount = gcodeData['layer_count'] 59 | 60 | self.size = gcodeData['size'] 61 | 62 | self.layerHeight = gcodeData['layer_height'] 63 | 64 | self.totalFilament = None#total_filament has not got any information 65 | 66 | self.readyCallback(self.layerList,self.totalPrintTime,self.layerCount,self.size,self.layerHeight,self.totalFilament,self.parent) 67 | 68 | 69 | except ValueError: 70 | self._logger.error("Bad gcode data returned: %s" % pipe.stdout.text) 71 | gcodeData = None 72 | 73 | if self.exceptionCallback: 74 | parameters = {} 75 | parameters['parent'] = self.parent 76 | parameters['filename'] = self.filename 77 | 78 | self.exceptionCallback(parameters) 79 | 80 | else: 81 | self._logger.warn('Error executing GCode Analyzer') 82 | gcodeData = None 83 | 84 | 85 | if self.exceptionCallback: 86 | parameters = {} 87 | parameters['parent'] = self.parent 88 | parameters['filename'] = self.filename 89 | 90 | self.exceptionCallback(parameters) 91 | 92 | except: 93 | self._logger.warn('Error running GCode Analyzer') 94 | gcodeData = None 95 | 96 | if self.exceptionCallback: 97 | parameters = {} 98 | parameters['parent'] = self.parent 99 | parameters['filename'] = self.filename 100 | 101 | self.exceptionCallback(parameters) 102 | -------------------------------------------------------------------------------- /octoprint_astroprint/materialcounter/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import re 9 | 10 | from copy import copy 11 | 12 | class MaterialCounter(object): 13 | 14 | #Extrusion modes 15 | EXTRUSION_MODE_ABSOLUTE = 1 16 | EXTRUSION_MODE_RELATIVE = 2 17 | 18 | def __init__(self, plugin): 19 | self.plugin = plugin 20 | self._logger = self.plugin.get_logger() 21 | self._extrusionMode = self.EXTRUSION_MODE_ABSOLUTE 22 | self._activeTool = "0" 23 | self._lastExtruderLengthReset = {"0": 0} 24 | self._consumedFilament = {"0": 0} 25 | self._lastExtrusion = {"0": 0} 26 | 27 | 28 | # regexes 29 | floatPattern = r"[-+]?[0-9]*\.?[0-9]+" 30 | intPattern = r"\d+" 31 | self._regex_paramEFloat = re.compile("E(%s)" % floatPattern) 32 | self._regex_paramTInt = re.compile("T(%s)" % intPattern) 33 | 34 | 35 | @property 36 | def extrusionMode(self): 37 | return self._extrusionMode 38 | 39 | @property 40 | def consumedFilament(self): 41 | if self._consumedFilament and self._extrusionMode == self.EXTRUSION_MODE_ABSOLUTE: 42 | tool = self._activeTool 43 | consumedFilament = copy(self._consumedFilament) 44 | 45 | try: 46 | consumedFilament[tool] += ( self._lastExtrusion[tool] - self._lastExtruderLengthReset[tool] ) 47 | 48 | except KeyError: 49 | return None 50 | 51 | return consumedFilament 52 | 53 | else: 54 | return { k: max(v,0) for k, v in self._consumedFilament.items() } #It can be negative because of retraction but we can't "consume" negative filament 55 | 56 | @property 57 | def totalConsumedFilament(self): 58 | consumedFilament = self.consumedFilament 59 | return sum([consumedFilament[k] for k in consumedFilament.keys()]) 60 | 61 | def startPrint(self): 62 | tool = self._activeTool 63 | self._lastExtruderLengthReset = {tool: 0} 64 | self._consumedFilament = {tool: 0} 65 | self._lastExtrusion = {tool: 0} 66 | 67 | 68 | def _gcode_T(self, cmd): #changeActiveTool 69 | toolMatch = self._regex_paramTInt.search(cmd) 70 | if toolMatch: 71 | tool = int(toolMatch.group(1)) 72 | if self._activeTool != tool: 73 | newTool = str(tool) 74 | oldTool = str(self._activeTool) 75 | #Make sure the head is registered 76 | if newTool not in self._consumedFilament: 77 | self._consumedFilament[newTool] = 0 78 | self._lastExtruderLengthReset[newTool] = 0 79 | self._lastExtrusion[newTool] = 0 80 | 81 | if self._extrusionMode == self.EXTRUSION_MODE_ABSOLUTE: 82 | if oldTool in self.consumedFilament and oldTool in self._lastExtrusion and oldTool in self._lastExtruderLengthReset: 83 | self.consumedFilament[oldTool] += ( self._lastExtrusion[oldTool] - self._lastExtruderLengthReset[oldTool] ) 84 | self._lastExtruderLengthReset[oldTool] = self.consumedFilament[oldTool] 85 | else: 86 | self._logger.error('Unkonwn previous tool %s when trying to change to new tool %s' % (oldTool, newTool)) 87 | self._activeTool = newTool 88 | 89 | def _gcode_G92(self, cmd): 90 | # At the moment this command is only relevant in Absolute Extrusion Mode 91 | if self._extrusionMode == self.EXTRUSION_MODE_ABSOLUTE: 92 | eValue = None 93 | 94 | if cmd.strip() == 'G92': #A simple G92 command resets all axis so E is now set to 0 95 | eValue = 0 96 | elif 'E' in cmd: 97 | match = self._regex_paramEFloat.search(cmd) 98 | if match: 99 | try: 100 | eValue = float(match.group(1)) 101 | 102 | except ValueError: 103 | pass 104 | 105 | if eValue is not None: 106 | #There has been an E reset 107 | #resetExtruderLength: 108 | tool = self._activeTool 109 | if self._extrusionMode == self.EXTRUSION_MODE_ABSOLUTE: 110 | # We add what we have to the total for the his tool head 111 | self._consumedFilament[tool] += ( self._lastExtrusion[tool] - self._lastExtruderLengthReset[tool] ) 112 | 113 | self._lastExtruderLengthReset[tool] = eValue 114 | self._lastExtrusion[tool] = eValue 115 | 116 | 117 | def _gcode_G0(self, cmd): 118 | if 'E' in cmd: 119 | match = self._regex_paramEFloat.search(cmd) 120 | if match: 121 | try: 122 | #reportExtrusion 123 | length = float(match.group(1)) 124 | if self._extrusionMode == self.EXTRUSION_MODE_RELATIVE: 125 | #if length > 0: #never report retractions 126 | self._consumedFilament[self._activeTool] += length 127 | 128 | else: # EXTRUSION_MODE_ABSOLUTE 129 | tool = self._activeTool 130 | 131 | #if length > self._lastExtrusion[tool]: #never report retractions 132 | self._lastExtrusion[tool] = length 133 | 134 | except ValueError: 135 | pass 136 | 137 | 138 | _gcode_G1 = _gcode_G0 139 | 140 | def _gcode_M82(self, cmd): #Set to absolute extrusion mode 141 | self._extrusionMode = self.EXTRUSION_MODE_ABSOLUTE 142 | 143 | 144 | def _gcode_M83(self, cmd): #Set to relative extrusion mode 145 | self._extrusionMode = self.EXTRUSION_MODE_RELATIVE 146 | tool = self._activeTool 147 | #it was absolute before so we add what we had to the active head counter 148 | self._consumedFilament[tool] += ( self._lastExtrusion[tool] - self._lastExtruderLengthReset[tool] ) 149 | 150 | 151 | # In Marlin G91 and G90 also change the relative nature of extrusion 152 | _gcode_G90 = _gcode_M82 #Set Absolute 153 | _gcode_G91 = _gcode_M83 #Set Relative 154 | -------------------------------------------------------------------------------- /octoprint_astroprint/printerlistener/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | __author__ = "AstroPrint Product Team " 5 | __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' 6 | __copyright__ = "Copyright (C) 2017-2020 3DaGoGo, Inc - Released under terms of the AGPLv3 License" 7 | 8 | import time 9 | 10 | from octoprint.printer import PrinterCallback 11 | from octoprint_astroprint.gCodeAnalyzer import GCodeAnalyzer 12 | 13 | class PrinterListener(PrinterCallback): 14 | 15 | def __init__(self, plugin): 16 | self.cameraManager = None 17 | self.astroprintCloud = None 18 | self._analyzed_job_layers = None 19 | 20 | self._router = None 21 | self._plugin = plugin 22 | self._printer = self._plugin.get_printer() 23 | self._logger = self._plugin.get_logger() 24 | self._progress = None 25 | self._state = None 26 | self._job_data = None 27 | self._currentLayer = None 28 | self._timePercentPreviuosLayers = None 29 | self.last_layer_time_percent = None 30 | self._last_time_send = None 31 | self._printStartedAt = None 32 | 33 | 34 | def addWatcher(self, socket): 35 | self._router = socket 36 | 37 | def removeWatcher(self): 38 | self._router = None 39 | 40 | def get_current_layer(self): 41 | return self._currentLayer 42 | 43 | def get_analyzed_job_layers(self): 44 | return self._analyzed_job_layers 45 | 46 | def startPrint(self, file): 47 | self._analyzed_job_layers = None 48 | self._currentLayer = 0 49 | self.last_layer_time_percent = 0 50 | self._timePercentPreviuosLayers = 0 51 | self._printStartedAt = None 52 | self.timerCalculator = GCodeAnalyzer(file,True,self.cbGCodeAnalyzerReady,self.cbGCodeAnalyzerFail,self, self._plugin) 53 | self.timerCalculator.makeCalcs() 54 | 55 | def cbGCodeAnalyzerReady(self,timePerLayers,totalPrintTime,layerCount,size,layer_height,total_filament,parent): 56 | self._analyzed_job_layers = {} 57 | self._analyzed_job_layers["timePerLayers"] = timePerLayers 58 | self._analyzed_job_layers["layerCount"] = layerCount 59 | self._analyzed_job_layers["totalPrintTime"] = totalPrintTime*1.07 60 | 61 | def cbGCodeAnalyzerFail(self, parameters): 62 | self._logger.error("Fail to analyze Gcode: %s" % parameters['filename']) 63 | 64 | def updateAnalyzedJobInformation(self, progress): 65 | #analyzedInformation = {"current_layer" : 0, "time_percent_previuos_layers" : 0} 66 | layerChanged = False 67 | if not self._currentLayer: 68 | self._currentLayer = 1 69 | 70 | if self._analyzed_job_layers: 71 | while self._analyzed_job_layers["timePerLayers"][self._currentLayer -1]['upperPercent'] < progress: 72 | layerChanged = True 73 | self._currentLayer+=1 74 | 75 | if layerChanged: 76 | if not self._currentLayer == 1: 77 | self._timePercentPreviuosLayers += self._analyzed_job_layers["timePerLayers"][self._currentLayer -2 ]['time'] 78 | else: 79 | self._timePercentPreviuosLayers = 0 80 | 81 | self.cameraManager.layerChanged() 82 | self._plugin.sendSocketInfo() 83 | 84 | def on_printer_add_temperature(self, data): 85 | if self._router: 86 | payload = {} 87 | 88 | if 'bed' in data: 89 | payload['bed'] = { 'actual': data['bed']['actual'], 'target': data['bed']['target'] } 90 | 91 | dataProfile = self._plugin._printer_profile_manager.get_current_or_default() 92 | extruder_count = dataProfile['extruder']['count'] 93 | for i in range(extruder_count): 94 | tool = 'tool'+str(i) 95 | if tool in data: 96 | payload[tool] = { 'actual': data[tool]['actual'], 'target': data[tool]['target'] } 97 | 98 | self._router.broadcastEvent('temp_update', payload) 99 | 100 | def on_printer_send_current_data(self, data): 101 | self.set_state(data) 102 | self.set_job_data(data['job']) 103 | self.set_progress(data) 104 | 105 | def set_state(self, data): 106 | flags = data['state']['flags'] 107 | payload = { 108 | 'operational': flags['operational'], 109 | 'printing': flags['printing'] or flags['paused'], 110 | 'paused': flags['paused'], 111 | 'camera': True, #self.cameraManager.cameraActive if self.cameraManager else None, 112 | 'heatingUp': self._plugin.printerIsHeating(), 113 | 'state': data['state']['text'].lower(), 114 | 'ready_to_print': self._plugin.isBedClear and flags['operational'] and not flags['printing'] and not flags['paused'], 115 | } 116 | if self._plugin.printerIsHeating(): 117 | self._last_time_send = 0 118 | if payload != self._state: 119 | self._plugin.sendSocketInfo() 120 | if self._router: 121 | self._router.broadcastEvent('status_update', payload) 122 | self._state = payload 123 | 124 | 125 | def set_job_data(self, data): 126 | if data['file']['name'] and data['file']['size']: 127 | renderedImage = None 128 | cloudId = None 129 | if self.astroprintCloud and data['file']['origin'] == 'local': 130 | cloudPrintFile = self.astroprintCloud.db.getPrintFileByOctoPrintPath(data['file']['path']) 131 | if cloudPrintFile: 132 | renderedImage = cloudPrintFile.renderedImage 133 | cloudId = cloudPrintFile.printFileId 134 | payload = { 135 | "estimatedPrintTime": data['estimatedPrintTime'], 136 | "layerCount": self._analyzed_job_layers['layerCount'] if self._analyzed_job_layers else None, 137 | "file": { 138 | "origin": data['file']['origin'], 139 | "rendered_image": renderedImage, 140 | "name": data['file']['name'], 141 | "cloudId": cloudId, 142 | "date": data['file']['date'], 143 | "printFileName":data['file']['name'], 144 | "size": data['file']['size'] 145 | }, 146 | "filament": data['filament'] 147 | } 148 | else: 149 | payload = None 150 | 151 | if payload != self._job_data: 152 | self._plugin.sendSocketInfo() 153 | self._job_data = payload 154 | 155 | def get_job_data(self): 156 | if not self._job_data: 157 | return None 158 | if not self._job_data['layerCount']: 159 | self._job_data['layerCount'] = self._analyzed_job_layers['layerCount'] if self._analyzed_job_layers else None 160 | return self._job_data 161 | 162 | def set_progress(self, data): 163 | if data['progress']['printTime']: 164 | payload = self.time_adjuster(data['progress']) 165 | else : 166 | self._last_time_send = 0 167 | payload= None 168 | if payload != self._progress and self._router: 169 | self._router.broadcastEvent('printing_progress', payload) 170 | self._progress = payload 171 | 172 | def get_progress(self): 173 | return self._progress 174 | 175 | 176 | def time_adjuster(self, data): 177 | payload = dict(data) 178 | if not self._printStartedAt: 179 | self._printStartedAt = payload['printTime'] 180 | if not self._analyzed_job_layers: 181 | payload['currentLayer'] = 0 182 | return payload 183 | else: 184 | self.updateAnalyzedJobInformation(payload['completion']/100) 185 | payload['currentLayer'] = self._currentLayer 186 | 187 | try: 188 | layerFileUpperPercent = self._analyzed_job_layers["timePerLayers"][self._currentLayer-1]['upperPercent'] 189 | 190 | if self._currentLayer > 1: 191 | layerFileLowerPercent = self._analyzed_job_layers["timePerLayers"][self._currentLayer-2]['upperPercent'] 192 | else: 193 | layerFileLowerPercent = 0 194 | 195 | currentAbsoluteFilePercent = payload['completion']/100 196 | elapsedTime = payload['printTime'] 197 | 198 | try: 199 | currentLayerPercent = (currentAbsoluteFilePercent - layerFileLowerPercent) / (layerFileUpperPercent - layerFileLowerPercent) 200 | except: 201 | currentLayerPercent = 0 202 | 203 | layerTimePercent = currentLayerPercent * self._analyzed_job_layers["timePerLayers"][self._currentLayer-1]['time'] 204 | 205 | currentTimePercent = self._timePercentPreviuosLayers + layerTimePercent 206 | 207 | estimatedTimeLeft = self._analyzed_job_layers["totalPrintTime"] * ( 1.0 - currentTimePercent ) 208 | 209 | elapsedTimeVariance = elapsedTime - ( self._analyzed_job_layers["totalPrintTime"] - estimatedTimeLeft) 210 | adjustedEstimatedTime = self._analyzed_job_layers["totalPrintTime"] + elapsedTimeVariance 211 | estimatedTimeLeft = ( adjustedEstimatedTime * ( 1.0 - currentTimePercent ) ) 212 | 213 | if payload['printTimeLeft'] and payload['printTimeLeft'] < estimatedTimeLeft: 214 | estimatedTimeLeft = payload['printTimeLeft'] 215 | #we prefer to freeze time rather than increase it 216 | if self._last_time_send > estimatedTimeLeft or self._last_time_send is 0: 217 | self._last_time_send = estimatedTimeLeft 218 | else: 219 | estimatedTimeLeft = self._last_time_send 220 | 221 | payload['currentLayer'] = self._currentLayer 222 | payload['printTimeLeft'] = estimatedTimeLeft 223 | 224 | return payload 225 | except Exception: 226 | return payload 227 | -------------------------------------------------------------------------------- /octoprint_astroprint/static/css/astroprint.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: AstroPrint Product Team 3 | * License: AGPLv3 4 | * Copyright: 2017 3DaGoGo Inc. 5 | */ 6 | #tab_plugin_astroprint { 7 | width: 100%; 8 | } 9 | #tab_plugin_astroprint .cloud-title { 10 | text-transform: uppercase; 11 | letter-spacing: 1px; 12 | color: #515151; 13 | margin-left: 25px; 14 | font-size: 14px; 15 | margin-top: 10px; 16 | padding: 0 5px; 17 | box-shadow: 0 3px #5e88de; 18 | } 19 | #tab_plugin_astroprint .edit-name { 20 | border-bottom: 3px solid rgba(0, 135, 205, 0.25); 21 | } 22 | #tab_plugin_astroprint .edit-name:hover { 23 | text-decoration: none; 24 | } 25 | #tab_plugin_astroprint .edit-name:hover i { 26 | opacity: 0.7; 27 | } 28 | #tab_plugin_astroprint .edit-name i.icon-edit { 29 | font-size: 26px; 30 | margin-left: 10px; 31 | color: #0087cd; 32 | } 33 | #tab_plugin_astroprint .astroPrintView { 34 | width: 100%; 35 | } 36 | #tab_plugin_astroprint .astroPrintView .auth_astro { 37 | width: 562px; 38 | margin-top: 10px; 39 | font-size: 12px; 40 | letter-spacing: 0.6px; 41 | } 42 | #tab_plugin_astroprint .astroPrintView .access_key { 43 | width: 573px; 44 | } 45 | #tab_plugin_astroprint .astroPrintView .warn-box { 46 | color: grey; 47 | border-radius: 5px; 48 | font-size: 12px; 49 | padding: 15px; 50 | line-height: 140%; 51 | background: rgba(255, 161, 21, 0.12); 52 | letter-spacing: 0.6px; 53 | } 54 | #tab_plugin_astroprint .astroPrintView .cloud-title { 55 | text-transform: uppercase; 56 | letter-spacing: 1px; 57 | color: #515151; 58 | margin-left: 25px; 59 | font-size: 14px; 60 | margin-top: 10px; 61 | padding: 0 5px; 62 | box-shadow: 0 3px #5e88de; 63 | } 64 | #tab_plugin_astroprint .astroPrintView .downloadDialog { 65 | background: rgba(10, 140, 204, 0.15); 66 | padding: 2px 10px; 67 | border-radius: 5px; 68 | color: #4d4d4d; 69 | } 70 | #tab_plugin_astroprint .astroPrintImageContainer { 71 | width: 150px; 72 | height: 150px; 73 | background-color: grey; 74 | } 75 | #tab_plugin_astroprint .astroPrintImageContainer .astroPrintImage { 76 | width: 100%; 77 | height: 100%; 78 | background-image: url(../img/Astroprint_square_logo.png); 79 | background-size: 150px; 80 | background-repeat: no-repeat; 81 | } 82 | #tab_plugin_astroprint .astroprint-nav-title { 83 | font-size: 20px; 84 | } 85 | #tab_plugin_astroprint .favicon-rocketImageContainer { 86 | width: 20px; 87 | height: 20px; 88 | display: inline-block; 89 | } 90 | #tab_plugin_astroprint .favicon-rocketImageContainer .favicon-rocket { 91 | width: 100%; 92 | height: 100%; 93 | background-image: url(../img/favicon_rocket.ico); 94 | background-size: 20px; 95 | background-repeat: no-repeat; 96 | } 97 | #tab_plugin_astroprint .row-fluid { 98 | width: 100%; 99 | padding: 3px; 100 | } 101 | #tab_plugin_astroprint #navbar-astroprint .camera-options { 102 | display: inline-block; 103 | padding-top: 11px; 104 | position: relative; 105 | } 106 | #tab_plugin_astroprint #navbar-astroprint .camera-options .remove-opt { 107 | position: absolute; 108 | color: #ffffff; 109 | cursor: pointer; 110 | left: 2px; 111 | } 112 | #tab_plugin_astroprint #navbar-astroprint .camera-options .camera-opt i { 113 | color: #f15252; 114 | cursor: pointer; 115 | font-size: 20px; 116 | } 117 | #tab_plugin_astroprint #navbar-astroprint .mobile-options { 118 | display: inline-block; 119 | padding-top: 11px; 120 | position: relative; 121 | } 122 | #tab_plugin_astroprint #navbar-astroprint .mobile-options .remove-opt { 123 | position: absolute; 124 | color: #ffffff; 125 | cursor: pointer; 126 | left: 2px; 127 | } 128 | #tab_plugin_astroprint #navbar-astroprint .mobile-options .mobile-opt i { 129 | color: #f15252; 130 | cursor: pointer; 131 | font-size: 20px; 132 | } 133 | #tab_plugin_astroprint .designs-view { 134 | width: 100%; 135 | padding: 10px; 136 | } 137 | #tab_plugin_astroprint .designs-view .design-row { 138 | width: 100%; 139 | padding: 5px 5px; 140 | margin: 0; 141 | border-top: 1px solid #DDD; 142 | } 143 | #tab_plugin_astroprint .designs-view .design-row .icon-angle-down { 144 | font-size: 26px; 145 | color: darkgrey; 146 | } 147 | #tab_plugin_astroprint .designs-view .design-row .design-info { 148 | padding: 0; 149 | } 150 | #tab_plugin_astroprint .designs-view .design-row .design-info .sections-design-row { 151 | margin-left: 0; 152 | } 153 | #tab_plugin_astroprint .designs-view .design-row .design-info .sections-design-row .center-button { 154 | width: 50%; 155 | height: 100%; 156 | margin: auto; 157 | display: block; 158 | font-size: 20px; 159 | } 160 | #tab_plugin_astroprint .designs-view .design-row .design-info .sections-design-row .center-button i.icon-chevron-down, 161 | #tab_plugin_astroprint .designs-view .design-row .design-info .sections-design-row .center-button i.icon-chevron-up { 162 | font-weight: normal; 163 | } 164 | #tab_plugin_astroprint .designs-view .design-row .design-info .sections-design-row .design-name { 165 | font-weight: bold; 166 | height: 40px; 167 | font-size: 14px; 168 | line-height: 40px; 169 | } 170 | #tab_plugin_astroprint .designs-view .design-row .design-info .sections-design-row .printFilesCount { 171 | height: 20px; 172 | line-height: 20px; 173 | font-size: 80%; 174 | color: #999; 175 | } 176 | #tab_plugin_astroprint .designs-view .design-row .design-info .designImageContainer { 177 | margin: auto; 178 | width: 60px; 179 | height: 60px; 180 | background-color: #DDD; 181 | } 182 | #tab_plugin_astroprint .designs-view .design-row .design-info .designImageContainer .designImage { 183 | width: 100%; 184 | height: 100%; 185 | } 186 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles { 187 | margin: 10px 5%; 188 | width: 77%; 189 | float: right; 190 | } 191 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row { 192 | width: 100%; 193 | border-bottom: 1px solid #DDD; 194 | margin: 0; 195 | clear: both; 196 | } 197 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info { 198 | clear: both; 199 | width: 100%; 200 | height: 50px; 201 | } 202 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .half-printFile-row { 203 | height: 25px; 204 | } 205 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .all-printFile-row { 206 | height: 50px; 207 | } 208 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .center-button { 209 | width: 30px; 210 | height: 30px; 211 | margin: auto; 212 | margin-top: 5px; 213 | display: block; 214 | } 215 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .printFileName { 216 | font-size: 12px; 217 | white-space: nowrap; 218 | text-overflow: ellipsis; 219 | overflow: hidden; 220 | } 221 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .slicerSettings { 222 | font-weight: bold; 223 | } 224 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .slicerSettings .infoSlicerSettings { 225 | font-size: 10px; 226 | } 227 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .main-info .printFileCreated { 228 | font-size: 12px; 229 | color: #999; 230 | } 231 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .more-printfile-info { 232 | font-size: 10px; 233 | clear: both; 234 | width: 83%; 235 | margin: 0px 13% 20px; 236 | } 237 | #tab_plugin_astroprint .designs-view .design-row .design-printfiles .print-file-row .more-printfile-info .info-row { 238 | clear: both; 239 | } 240 | #tab_plugin_astroprint .design-printfiles .print-file-row { 241 | width: 100%; 242 | padding: 5px 5px; 243 | margin: 0; 244 | border-bottom: 1px solid #DDD; 245 | } 246 | #tab_plugin_astroprint .design-printfiles .print-file-row .printFileName { 247 | margin-bottom: 5px; 248 | } 249 | #tab_plugin_astroprint .design-printfiles .print-file-row .slicerSettings { 250 | padding-right: 10px; 251 | line-height: 110%; 252 | font-weight: bold; 253 | } 254 | #tab_plugin_astroprint .design-printfiles .print-file-row .printFileCreated { 255 | opacity: 0.6; 256 | } 257 | #tab_plugin_astroprint .design-printfiles .print-file-row .icon-angle-down { 258 | font-size: 26px; 259 | color: darkgrey; 260 | } 261 | #tab_plugin_astroprint .design-printfiles .print-file-row .more-printfile-info .info-row .span4 { 262 | text-transform: uppercase; 263 | letter-spacing: 2px; 264 | font-size: 10px; 265 | color: darkgrey; 266 | } 267 | #tab_plugin_astroprint .design-printfiles .print-file-row .more-printfile-info .info-row:first-of-type { 268 | margin-top: 10px; 269 | padding-top: 10px; 270 | border-top: 1px dashed #cacaca; 271 | } 272 | #tab_plugin_astroprint .designs-loading { 273 | height: 300px; 274 | padding: 20px; 275 | } 276 | #tab_plugin_astroprint .designs-loading div { 277 | width: 100%; 278 | display: inline-block; 279 | margin: auto; 280 | } 281 | #tab_plugin_astroprint .designs-loading div p { 282 | text-align: center; 283 | } 284 | #tab_plugin_astroprint .designs-loading div div { 285 | display: block; 286 | width: 100px; 287 | margin-top: 75px; 288 | margin: auto; 289 | } 290 | #tab_plugin_astroprint .designs-loading div div i { 291 | font-size: 75px; 292 | } 293 | #tab_plugin_astroprint .designs-loading div .error-retrieving { 294 | color: red; 295 | } 296 | #tab_plugin_astroprint .bold { 297 | font-weight: bold; 298 | } 299 | #settings_dialog .edit-name { 300 | border-bottom: 3px solid rgba(0, 135, 205, 0.25); 301 | } 302 | #settings_dialog .edit-name:hover { 303 | text-decoration: none; 304 | } 305 | #settings_dialog .edit-name:hover i { 306 | opacity: 0.7; 307 | } 308 | #settings_dialog .edit-name i.icon-edit { 309 | font-size: 26px; 310 | margin-left: 10px; 311 | color: #0087cd; 312 | } 313 | #settings_dialog .control-group { 314 | margin-bottom: 0; 315 | } 316 | #settings_dialog .control-group .blink { 317 | animation: blink-animation 0.5s steps(5, start) infinite; 318 | -webkit-animation: blink-animation 0.5s steps(5, start) infinite; 319 | } 320 | @keyframes blink-animation { 321 | to { 322 | visibility: hidden; 323 | } 324 | } 325 | @-webkit-keyframes blink-animation { 326 | to { 327 | visibility: hidden; 328 | } 329 | } 330 | #settings_dialog .control-group .icon-circle { 331 | color: #66ca66; 332 | } 333 | #settings_dialog .control-group .scan-camera-button { 334 | margin-top: 10px; 335 | background: #0088cb; 336 | padding: 5px 10px; 337 | max-width: 130px; 338 | border-radius: 2px; 339 | color: white; 340 | text-align: center; 341 | margin-top: 5px; 342 | } 343 | #settings_dialog .control-group .scan-camera-button i.icon-facetime-video { 344 | margin-right: 5px; 345 | } 346 | #settings_dialog .control-group .no-camera .icon-circle { 347 | color: #f15252; 348 | } 349 | #settings_dialog .control-group .warn-box { 350 | margin-top: 10px; 351 | color: grey; 352 | border-radius: 5px; 353 | font-size: 12px; 354 | padding: 15px; 355 | line-height: 140%; 356 | background: rgba(255, 161, 21, 0.12); 357 | letter-spacing: 0.6px; 358 | } 359 | #settings_dialog .control-group h4 { 360 | margin-top: 0; 361 | } 362 | #settings_dialog .control-group .fa-trash-o { 363 | color: #f05151; 364 | font-size: 28px; 365 | margin-left: 10px; 366 | } 367 | #settings_dialog .control-group i.icon-edit { 368 | font-size: 26px; 369 | margin-left: 8px; 370 | color: #0087cd; 371 | text-decoration: none; 372 | border: 0; 373 | } 374 | #settings_dialog .control-group i.icon-edit:hover { 375 | opacity: 0.7; 376 | } 377 | #settings_dialog .control-group .filament-icon { 378 | text-align: center; 379 | width: 26px; 380 | height: 26px; 381 | display: inline-block; 382 | border: 1px solid gainsboro; 383 | padding: 2px; 384 | vertical-align: middle; 385 | margin-right: 5px; 386 | } 387 | #changeFilament .filament-color > div { 388 | width: 36px; 389 | height: 36px; 390 | display: inline-block; 391 | margin: 2px; 392 | border-radius: 3px; 393 | } 394 | #changeFilament .filament-color > div:hover { 395 | opacity: 0.8; 396 | cursor: pointer; 397 | } 398 | #changeFilament .fields { 399 | margin-bottom: 15px; 400 | } 401 | #changeFilament .fields input#filament_color { 402 | width: 100%; 403 | } 404 | -------------------------------------------------------------------------------- /octoprint_astroprint/static/img/Astroprint_square_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroPrint/OctoPrint-AstroPrint/bee4f3fd666f8a3a78b9442b15881d6ce5486dfe/octoprint_astroprint/static/img/Astroprint_square_logo.png -------------------------------------------------------------------------------- /octoprint_astroprint/static/img/favicon_rocket.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroPrint/OctoPrint-AstroPrint/bee4f3fd666f8a3a78b9442b15881d6ce5486dfe/octoprint_astroprint/static/img/favicon_rocket.ico -------------------------------------------------------------------------------- /octoprint_astroprint/static/less/astroprint.less: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: AstroPrint Product Team 3 | * License: AGPLv3 4 | * Copyright: 2017 3DaGoGo Inc. 5 | */ 6 | 7 | #tab_plugin_astroprint { 8 | width: 100%; 9 | 10 | .cloud-title{ 11 | text-transform: uppercase; 12 | letter-spacing: 1px; 13 | color: #515151; 14 | margin-left: 25px; 15 | font-size: 14px; 16 | margin-top: 10px; 17 | padding: 0 5px; 18 | box-shadow: 0 3px #5e88de; 19 | } 20 | 21 | 22 | .edit-name{ 23 | border-bottom: 3px solid rgba(0, 135, 205, 0.25); 24 | &:hover{ 25 | text-decoration: none; 26 | 27 | i{opacity: 0.7;} 28 | } 29 | 30 | i.icon-edit{ 31 | font-size: 26px; 32 | margin-left: 10px; 33 | color: rgba(0, 135, 205, 1); 34 | } 35 | } 36 | 37 | .astroPrintView { 38 | width: 100%; 39 | 40 | .auth_astro{ 41 | width: 562px; 42 | margin-top: 10px; 43 | font-size: 12px; 44 | letter-spacing: 0.6px; 45 | } 46 | 47 | .access_key{ 48 | width: 573px; 49 | } 50 | 51 | .warn-box{ 52 | color: grey; 53 | border-radius: 5px; 54 | font-size: 12px; 55 | padding: 15px; 56 | line-height: 140%; 57 | background: rgba(255, 161, 21, 0.12); 58 | letter-spacing: 0.6px; 59 | } 60 | 61 | .cloud-title{ 62 | text-transform: uppercase; 63 | letter-spacing: 1px; 64 | color: #515151; 65 | margin-left: 25px; 66 | font-size: 14px; 67 | margin-top: 10px; 68 | padding: 0 5px; 69 | box-shadow: 0 3px #5e88de; 70 | } 71 | 72 | .downloadDialog{ 73 | background: rgba(10, 140, 204, 0.15); 74 | padding: 2px 10px; 75 | border-radius: 5px; 76 | color: #4d4d4d; 77 | } 78 | } 79 | 80 | .astroPrintImageContainer{ 81 | width: 150px; 82 | height: 150px; 83 | background-color: grey; 84 | 85 | 86 | .astroPrintImage{ 87 | width: 100%; 88 | height: 100%; 89 | background-image: url(../img/Astroprint_square_logo.png); 90 | background-size: 150px; 91 | background-repeat: no-repeat; 92 | } 93 | } 94 | .astroprint-nav-title{ 95 | font-size: 20px; 96 | } 97 | 98 | .favicon-rocketImageContainer{ 99 | width: 20px; 100 | height: 20px; 101 | display: inline-block; 102 | 103 | .favicon-rocket{ 104 | width: 100%; 105 | height: 100%; 106 | background-image: url(../img/favicon_rocket.ico); 107 | background-size: 20px; 108 | background-repeat: no-repeat; 109 | } 110 | } 111 | 112 | .row-fluid{ 113 | width: 100%; 114 | padding: 3px; 115 | } 116 | 117 | #navbar-astroprint{ 118 | .camera-options{ 119 | display: inline-block; 120 | padding-top: 11px; 121 | position: relative; 122 | 123 | .remove-opt{ 124 | position: absolute; 125 | color: #ffffff; 126 | cursor: pointer; 127 | left: 2px; 128 | } 129 | 130 | .camera-opt i{ 131 | color: #f15252; 132 | cursor: pointer; 133 | font-size: 20px; 134 | } 135 | } 136 | .mobile-options{ 137 | display: inline-block; 138 | padding-top: 11px; 139 | position: relative; 140 | 141 | .remove-opt{ 142 | position: absolute; 143 | color: #ffffff; 144 | cursor: pointer; 145 | left: 2px; 146 | } 147 | 148 | .mobile-opt i{ 149 | color: #f15252; 150 | cursor: pointer; 151 | font-size: 20px; 152 | } 153 | } 154 | } 155 | 156 | .designs-view{ 157 | width: 100%; 158 | padding : 10px; 159 | .design-row{ 160 | width: 100%; 161 | padding : 5px 5px; 162 | margin: 0; 163 | border-top: 1px solid #DDD; 164 | 165 | .icon-angle-down{ 166 | font-size: 26px; 167 | color: darkgrey; 168 | } 169 | 170 | .design-info{ 171 | padding : 0; 172 | .sections-design-row{ 173 | margin-left: 0; 174 | .center-button{ 175 | width: 50%; 176 | height: 100%; 177 | margin: auto; 178 | display: block; 179 | font-size: 20px; 180 | i.icon-chevron-down, i.icon-chevron-up { 181 | //color : #08c; 182 | font-weight: normal; 183 | } 184 | } 185 | .design-name{ 186 | font-weight: bold; 187 | height: 40px; 188 | font-size: 14px; 189 | line-height: 40px; 190 | 191 | } 192 | .printFilesCount{ 193 | height: 20px; 194 | line-height: 20px; 195 | font-size: 80%; 196 | color: #999; 197 | } 198 | } 199 | .designImageContainer{ 200 | margin : auto; 201 | width: 60px; 202 | height: 60px; 203 | background-color: #DDD; 204 | .designImage{ 205 | width: 100%; 206 | height: 100%; 207 | } 208 | } 209 | } 210 | .design-printfiles{ 211 | margin : 10px 5%; 212 | width: 77%; 213 | float: right; 214 | .print-file-row { 215 | width: 100%; 216 | border-bottom: 1px solid #DDD; 217 | margin : 0; 218 | clear: both; 219 | .main-info{ 220 | clear : both; 221 | width: 100%; 222 | height: 50px; 223 | .half-printFile-row{ 224 | height: 25px; 225 | } 226 | .all-printFile-row{ 227 | height: 50px; 228 | } 229 | .center-button{ 230 | width: 30px; 231 | height: 30px; 232 | margin: auto; 233 | margin-top: 5px; 234 | display: block 235 | } 236 | .printFileName{ 237 | font-size: 12px; 238 | white-space: nowrap; 239 | text-overflow: ellipsis; 240 | overflow: hidden; 241 | } 242 | .slicerSettings{ 243 | font-weight: bold; 244 | .infoSlicerSettings{ 245 | font-size: 10px; 246 | } 247 | } 248 | .printFileCreated{ 249 | font-size: 12px; 250 | color: #999; 251 | } 252 | } 253 | .more-printfile-info{ 254 | font-size: 10px; 255 | clear : both; 256 | width: 83%; 257 | margin: 0px 13% 20px; 258 | 259 | .info-row { 260 | clear : both; 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | 268 | .design-printfiles .print-file-row{ 269 | width: 100%; 270 | padding: 5px 5px; 271 | margin: 0; 272 | border-bottom: 1px solid #DDD; 273 | 274 | .printFileName{margin-bottom: 5px;} 275 | 276 | .slicerSettings{ 277 | padding-right: 10px; 278 | line-height: 110%; 279 | font-weight: bold; 280 | } 281 | 282 | .printFileCreated{ 283 | opacity: 0.6; 284 | } 285 | .icon-angle-down{ 286 | font-size: 26px; 287 | color: darkgrey; 288 | } 289 | 290 | .more-printfile-info{ 291 | .info-row .span4{ 292 | text-transform: uppercase; 293 | letter-spacing: 2px; 294 | font-size: 10px; 295 | color: darkgrey; 296 | } 297 | 298 | .info-row:first-of-type{ 299 | margin-top: 10px; 300 | padding-top: 10px; 301 | border-top: 1px dashed #cacaca; 302 | } 303 | } 304 | } 305 | 306 | .designs-loading { 307 | height: 300px; 308 | padding: 20px; 309 | div{ 310 | width: 100%; 311 | display:inline-block; 312 | margin: auto; 313 | p{ 314 | text-align: center 315 | } 316 | div{ 317 | display:block; 318 | width: 100px; 319 | margin-top: 75px; 320 | margin: auto; 321 | i{ 322 | font-size: 75px; 323 | } 324 | } 325 | .error-retrieving{ 326 | color: red; 327 | } 328 | } 329 | } 330 | 331 | .bold { 332 | font-weight: bold; 333 | } 334 | 335 | } 336 | 337 | #settings_dialog{ 338 | 339 | .edit-name{ 340 | border-bottom: 3px solid rgba(0, 135, 205, 0.25); 341 | &:hover{ 342 | text-decoration: none; 343 | 344 | i{opacity: 0.7;} 345 | } 346 | 347 | i.icon-edit{ 348 | font-size: 26px; 349 | margin-left: 10px; 350 | color: rgba(0, 135, 205, 1); 351 | } 352 | } 353 | 354 | .control-group{ 355 | margin-bottom: 0; 356 | .blink { 357 | animation: blink-animation 0.5s steps(5, start) infinite; 358 | -webkit-animation: blink-animation 0.5s steps(5, start) infinite; 359 | } 360 | @keyframes blink-animation { 361 | to { 362 | visibility: hidden; 363 | } 364 | } 365 | @-webkit-keyframes blink-animation { 366 | to { 367 | visibility: hidden; 368 | } 369 | } 370 | 371 | .icon-circle{ 372 | color: #66ca66; 373 | } 374 | 375 | .scan-camera-button{ 376 | margin-top: 10px; 377 | background: #0088cb; 378 | padding: 5px 10px; 379 | max-width: 130px; 380 | border-radius: 2px; 381 | color: white; 382 | text-align: center; 383 | margin-top: 5px; 384 | 385 | i.icon-facetime-video{margin-right: 5px;} 386 | } 387 | 388 | .no-camera{ 389 | .icon-circle { 390 | color: #f15252; 391 | } 392 | } 393 | 394 | .warn-box{ 395 | margin-top: 10px; 396 | color: grey; 397 | border-radius: 5px; 398 | font-size: 12px; 399 | padding: 15px; 400 | line-height: 140%; 401 | background: rgba(255, 161, 21, 0.12); 402 | letter-spacing: 0.6px; 403 | } 404 | 405 | h4{margin-top: 0;} 406 | 407 | .fa-trash-o{ 408 | color: #f05151; 409 | font-size: 28px; 410 | margin-left: 10px; 411 | } 412 | 413 | i.icon-edit{ 414 | font-size: 26px; 415 | margin-left: 8px; 416 | color: rgba(0, 135, 205, 1); 417 | text-decoration: none; 418 | border: 0; 419 | 420 | &:hover{opacity: 0.7;} 421 | } 422 | 423 | .filament-icon{ 424 | text-align: center; 425 | width: 26px; 426 | height: 26px; 427 | display: inline-block; 428 | border: 1px solid gainsboro; 429 | padding: 2px; 430 | vertical-align: middle; 431 | margin-right: 5px; 432 | } 433 | } 434 | } 435 | 436 | #changeFilament{ 437 | .filament-color >div { 438 | width: 36px; 439 | height: 36px; 440 | display: inline-block; 441 | margin: 2px; 442 | border-radius: 3px; 443 | 444 | &:hover{opacity: 0.8; cursor: pointer;} 445 | } 446 | 447 | .fields{ 448 | margin-bottom: 15px; 449 | 450 | input#filament_color{width: 100%;} 451 | } 452 | } 453 | 454 | -------------------------------------------------------------------------------- /octoprint_astroprint/templates/astroprint_settings.jinja2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Connect to your AstroPrint Account

5 |
6 |
7 |

To use and configure this plugin you need to connect it to your AstroPrint Account.

8 |

You can see more instructions in the AstroPrint Tab.

9 |
10 |
11 |
12 |
Open AstroPrint Tab
13 |
14 |
15 |
16 |

Camera

17 |
18 |
19 | Camera connected 20 |
21 |
22 |
Camera not connected
23 |
Scan for Camera
24 |
25 |
26 | 27 |
28 |
29 | 30 |

Printer Model

31 |
32 |
33 |

34 | 35 | 36 | 37 |

38 |
39 |
40 |

41 | Link Printer Model 42 |

43 |
44 |
45 |

Printer Filament

46 |
47 |
48 |

49 |
50 |
51 | 52 | 53 | 54 |

55 |
56 |
57 |

58 | Add Filament Info 59 |

60 |
61 |
62 |

Printer Settings

63 |
64 |
65 | 66 |
67 | 68 | °C 69 |
70 |
71 |
72 | 73 |
74 | 75 | °C 76 |
77 |
78 |
79 | 80 | 84 |
85 |
86 |
87 |

Remote Control Options

88 |
89 |
90 | 91 |

Cross Origin Resource Sharing

92 |

Allowing Cross Origin Resource Sharing is not enabled and it's necesary to control this box via the iOS App, it can be activated in API settings

93 | Go to API settings 94 |
95 |
96 |
97 |
98 |
128 | 141 | 171 | 184 | 185 | -------------------------------------------------------------------------------- /octoprint_astroprint/templates/astroprint_tab.jinja2: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |
16 |

Starting Plugin...

17 |
18 | 19 |
20 |
21 |
22 |
23 | 24 | 25 | 41 | 42 | 43 | 77 | 78 | 79 | 478 | 479 | 501 | 502 | 515 | -------------------------------------------------------------------------------- /octoprint_astroprint/util/AstroprintGCodeAnalyzer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroPrint/OctoPrint-AstroPrint/bee4f3fd666f8a3a78b9442b15881d6ce5486dfe/octoprint_astroprint/util/AstroprintGCodeAnalyzer -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ### 2 | # This file is only here to make sure that something like 3 | # 4 | # pip install -e . 5 | # 6 | # works as expected. Requirements can be found in setup.py. 7 | ### 8 | 9 | . 10 | ws4py==0.3.4 11 | requests_toolbelt==0.8.0 12 | Pillow==3.1.0 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | ######################################################################################################################## 4 | ### Do not forget to adjust the following variables to your own plugin. 5 | 6 | # The plugin's identifier, has to be unique 7 | plugin_identifier = "astroprint" 8 | 9 | # The plugin's python package, should be "octoprint_", has to be unique 10 | plugin_package = "octoprint_astroprint" 11 | 12 | # The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the 13 | # plugin module 14 | plugin_name = "AstroPrint" 15 | 16 | # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module 17 | plugin_version = "1.7.0" 18 | 19 | # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin 20 | # module 21 | plugin_description = "Wirelessly manage and monitor your OctoPi powered printer from the AstroPrint Platform (AP Mobile Apps, AP Desktop Software, & AP Web Portal)." 22 | 23 | # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module 24 | plugin_author = "AstroPrint Product Team" 25 | 26 | # The plugin's author's mail address. 27 | plugin_author_email = "product@astroprint.com" 28 | 29 | # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module 30 | plugin_url = "https://github.com/AstroPrint/OctoPrint-AstroPrint" 31 | 32 | # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module 33 | plugin_license = "AGPLv3" 34 | 35 | # Any additional requirements besides OctoPrint should be listed here 36 | plugin_requires = ["ws4py== 0.3.4","requests_toolbelt==0.10.1", "Pillow", "urllib3<2.0.0"] 37 | 38 | ### -------------------------------------------------------------------------------------------------------------------- 39 | ### More advanced options that you usually shouldn't have to touch follow after this point 40 | ### -------------------------------------------------------------------------------------------------------------------- 41 | 42 | # Additional package data to install for this plugin. The subfolders "templates", "static" and "translations" will 43 | # already be installed automatically if they exist. Note that if you add something here you'll also need to update 44 | # MANIFEST.in to match to ensure that python setup.py sdist produces a source distribution that contains all your 45 | # files. This is sadly due to how python's setup.py works, see also http://stackoverflow.com/a/14159430/2028598 46 | plugin_additional_data = [ 47 | ] 48 | 49 | # Any additional python packages you need to install with your plugin that are not contained in .* 50 | plugin_additional_packages = [ 51 | ] 52 | 53 | # Any python packages within .* you do NOT want to install with your plugin 54 | plugin_ignored_packages = [] 55 | 56 | # Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, 57 | # define dependency links or other things like that, this is the place to go. Will be merged recursively with the 58 | # default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using 59 | # octoprint.util.dict_merge. 60 | # 61 | # Example: 62 | # plugin_requires = ["someDependency==dev"] 63 | # additional_setup_parameters = {"dependency_links": ["https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} 64 | 65 | additional_setup_parameters = {} 66 | ######################################################################################################################## 67 | 68 | from setuptools import setup 69 | 70 | try: 71 | import octoprint_setuptools 72 | except: 73 | print("Could not import OctoPrint's setuptools, are you sure you are running that under " 74 | "the same python installation that OctoPrint is installed under?") 75 | import sys 76 | sys.exit(-1) 77 | 78 | setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( 79 | identifier=plugin_identifier, 80 | package=plugin_package, 81 | name=plugin_name, 82 | version=plugin_version, 83 | description=plugin_description, 84 | author=plugin_author, 85 | mail=plugin_author_email, 86 | url=plugin_url, 87 | license=plugin_license, 88 | requires=plugin_requires, 89 | additional_packages=plugin_additional_packages, 90 | ignored_packages=plugin_ignored_packages, 91 | additional_data=plugin_additional_data 92 | ) 93 | 94 | if len(additional_setup_parameters): 95 | from octoprint.util import dict_merge 96 | setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) 97 | 98 | setup(**setup_parameters) 99 | -------------------------------------------------------------------------------- /translations/README.txt: -------------------------------------------------------------------------------- 1 | Your plugin's translations will reside here. The provided setup.py supports a 2 | couple of additional commands to make managing your translations easier: 3 | 4 | babel_extract 5 | Extracts any translateable messages (marked with Jinja's `_("...")` or 6 | JavaScript's `gettext("...")`) and creates the initial `messages.pot` file. 7 | babel_refresh 8 | Reruns extraction and updates the `messages.pot` file. 9 | babel_new --locale= 10 | Creates a new translation folder for locale ``. 11 | babel_compile 12 | Compiles the translations into `mo` files, ready to be used within 13 | OctoPrint. 14 | babel_pack --locale= [ --author= ] 15 | Packs the translation for locale `` up as an installable 16 | language pack that can be manually installed by your plugin's users. This is 17 | interesting for languages you can not guarantee to keep up to date yourself 18 | with each new release of your plugin and have to depend on contributors for. 19 | 20 | If you want to bundle translations with your plugin, create a new folder 21 | `octoprint_astroprint/translations`. When that folder exists, 22 | an additional command becomes available: 23 | 24 | babel_bundle --locale= 25 | Moves the translation for locale `` to octoprint_astroprint/translations, 26 | effectively bundling it with your plugin. This is interesting for languages 27 | you can guarantee to keep up to date yourself with each new release of your 28 | plugin. 29 | --------------------------------------------------------------------------------