├── .gitattributes ├── octoprint_DuCalibrator ├── static │ ├── less │ │ └── DuCalibrator.less │ ├── css │ │ └── DuCalibrator.css │ └── js │ │ ├── DuCalCommon.js │ │ ├── trilateration.js │ │ ├── DuCalGeometry.js │ │ ├── DuCalMachine.js │ │ ├── TrackballControls.js │ │ └── DuCalViewModel.js ├── templates │ ├── Parameters.jinja2 │ ├── DuCalibrator_tab.jinja2 │ ├── History.jinja2 │ ├── DuCalibrator_settings.jinja2 │ ├── Probing.jinja2 │ ├── Instructions.md │ ├── Geometry.jinja2 │ └── Calibration.jinja2 └── __init__.py ├── .gitignore ├── babel.cfg ├── MANIFEST.in ├── requirements.txt ├── .editorconfig ├── extras ├── README.txt └── DuCalibrator.md ├── translations └── README.txt ├── README.md ├── setup.py └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/less/DuCalibrator.less: -------------------------------------------------------------------------------- 1 | // TODO: Put your plugin's LESS here, have it generated to ../css. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea 4 | *.iml 5 | build 6 | dist 7 | *.egg* 8 | .DS_Store 9 | *.zip 10 | .vs/ 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include octoprint_DuCalibrator/templates * 3 | recursive-include octoprint_DuCalibrator/translations * 4 | recursive-include octoprint_DuCalibrator/static * 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/css/DuCalibrator.css: -------------------------------------------------------------------------------- 1 | /* TODO: Have your plugin's CSS files generated to here. */ 2 | a.collapsed .fa-caret-down { 3 | display: none; 4 | } 5 | 6 | a.collapsed .fa-caret-right { 7 | display: inline-block; 8 | } 9 | 10 | a .fa-caret-right { 11 | display: none; 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [**.py] 13 | indent_style = tab 14 | 15 | [**.js] 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /extras/README.txt: -------------------------------------------------------------------------------- 1 | Currently Cookiecutter generates the following helpful extras to this folder: 2 | 3 | DuCalibrator.md 4 | Data file for plugins.octoprint.org. Fill in the missing TODOs once your 5 | plugin is ready for release and file a PR as described at 6 | http://plugins.octoprint.org/help/registering/ to get it published. 7 | 8 | This folder may be safely removed if you don't need it. 9 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/Parameters.jinja2: -------------------------------------------------------------------------------- 1 |
2 | {{ _('Probing') }} 3 |
4 | 5 |
6 |
7 | 8 | mm 9 |
10 | Radius of the probed area, ideally as far as the machine can reach. 11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 | Number of probing points, the more the merrier, tune to your patience and CPU power. 20 |
21 |
22 |
-------------------------------------------------------------------------------- /extras/DuCalibrator.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: plugin 3 | 4 | id: DuCalibrator 5 | title: Delta Micro Calibrator 6 | description: An OctoPrint Plugin for performing micro calibration in linear delta machines. 7 | author: Fabio Santos 8 | license: AGPLv3 9 | 10 | date: 2020-06-24 11 | 12 | homepage: https://github.com/Fabi0San/DuCalibrator 13 | source: https://github.com/Fabi0San/DuCalibrator 14 | archive: https://github.com/Fabi0San/DuCalibrator/archive/master.zip 15 | 16 | tags: 17 | - delta 18 | - calibration 19 | - 3d 20 | - visualization 21 | 22 | screenshots: 23 | - url: /assets/img/plugins/DuCalibrator/screenshot.png 24 | alt: Screenshot 25 | caption: Main calibration tab showing the full calibration workflow. 26 | 27 | compatibility: 28 | python: ">=2.7,<4" 29 | 30 | featuredimage: /assets/img/plugins/DuCalibrator/screenshot.png 31 | 32 | --- 33 | 34 | # Delta Micro Calibrator "ΔµCalibrator" 35 | An [OctoPrint](https://octoprint.org/) Plugin for performing micro calibration in linear delta machines. 36 | 37 | [README](https://github.com/Fabi0San/DuCalibrator/blob/master/README.md) 38 | 39 | ## Key features: 40 | * Automatic probing 41 | * Calibrates up to 18 geometry parameters 42 | * 3D visualization of probed points and estimated points after proposed calibration 43 | 44 | ![Screenshot](/assets/img/plugins/DuCalibrator/screenshot.png) 45 | 46 | ## Demo 47 | 48 | {% include youtube.html vid="kTJlOl7_X2M" preview="'/assets/img/plugins/DuCalibrator/screenshot.png'" %} 49 | 50 | Watch it in full screen 51 | -------------------------------------------------------------------------------- /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_DuCalibrator/translations`. When that folder exists, 22 | an additional command becomes available: 23 | 24 | babel_bundle --locale= 25 | Moves the translation for locale `` to octoprint_DuCalibrator/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 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/DuCalibrator_tab.jinja2: -------------------------------------------------------------------------------- 1 |
2 | 13 | 14 |
15 | 16 |
17 | {% filter markdown %}{% include "Instructions.md" %}{% endfilter %} 18 |
19 | 20 |
21 | {% include "Parameters.jinja2" %} 22 |
23 | 24 |
25 | 26 |
27 | {{ _('Please connect your printer for calibration') }} 28 |
29 | 30 | {% include "Geometry.jinja2" %} 31 | 32 | {% include "Probing.jinja2" %} 33 | 34 | {% include "Calibration.jinja2" %} 35 | 36 | {% include "History.jinja2" %} 37 | 38 |
39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delta Micro Calibrator "ΔµCalibrator" 2 | An [OctoPrint](https://octoprint.org/) Plugin for performing micro calibration in linear delta machines. 3 | 4 | ## Key features: 5 | * Automatic probing 6 | * Calibrates up to 18 geometry parameters 7 | * 3D visualization of probed points and estimated points after proposed calibration 8 | 9 | ![Screenshot](https://imgur.com/oT6HA8l.png) 10 | 11 | ## Requirements 12 | * You must have a Z probe. 13 | * The bed should be very flat. If not, consider dropping a boro glass on top of it just for the calibration and adjust height after glass is removed. 14 | * Currently, only Marlin firmware is supported, other firmwares can be added with some community help. [Check this file](https://github.com/Fabi0San/DuCalibrator/blob/master/octoprint_DuCalibrator/static/js/DuCalMachine.js) 15 | 16 | 17 | ## Setup 18 | 19 | Install via the bundled [Plugin Manager](https://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html) 20 | or manually using this URL: 21 | 22 | https://github.com/Fabi0San/DuCalibrator/archive/master.zip 23 | 24 | ## [Instructions](https://github.com/Fabi0San/DuCalibrator/blob/devel/octoprint_DuCalibrator/templates/Instructions.md) 25 | 26 | ## TODO 27 | * Add Smoothieware support (missing M118(echo) command). 28 | * Add XYZ probing using a 123 block as calibration target instead of assumedly flat bed for unambiguous calibration of all factors. 29 | 30 | ## Acknowledgements 31 | 32 | I was originally inspired by the generous work of [David Crocker](https://github.com/dc42) on his [calibration calculator](https://escher3d.com/pages/wizards/wizarddelta.php) and 33 | [Gina Häußge](https://github.com/foosel) on [OctoPrint](https://github.com/OctoPrint/OctoPrint). 34 | 35 | 36 | This plugin leverages [Three.js](https://threejs.org/) and [trilateration.js](https://github.com/gheja/trilateration.js). 37 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/History.jinja2: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | {{ _('Probing history') }}

5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 |
{{ _('Timestamp') }}{{ _('RMS') }}{{ _('Name') }}{{ _('Action') }}
22 | 24 |  |  25 | 26 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/DuCalibrator_settings.jinja2: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{ _('Machine settings') }} 5 |
6 | 7 |
8 |
9 | 13 |
14 | The machine type and firmware being used. 15 |
16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 | Initialization commands to be send to the machine before every probe, at the very least homing command. 24 |
25 |
26 |
27 | 28 |
29 |
30 | 31 | mm 32 |
33 | Safe height that the nozzle can travel from probe point to probe point without dragging the probe across the bed. Should be bigger than your probe z offset. 34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/Probing.jinja2: -------------------------------------------------------------------------------- 1 |
2 | 9 |

10 | 11 | {{ _('Probing') }} 12 | 13 | 14 |

15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 | 31 |
32 |
33 | 37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/Instructions.md: -------------------------------------------------------------------------------- 1 | # Instructions 2 | 3 | #### 1. **Perform initial calibration elsewhere.** 4 | This plugin's main goal is to find micron level delta geometry parameters, so please perform initial delta calibration using other methods. 5 | 6 | #### 2. **Configure settings** 7 | Make sure to configure this plugin to your machine in OctoPrint's settings dialog. 8 | 9 | #### 3. **Set Parameters** 10 | Set the probing parameters. 11 | 12 | #### 4. **Calibrate** 13 | 1. Fetch Geometry 14 | 2. Probe bed 15 | 3. Select calibration factors 16 | 4. Apply 17 | 5. Repeat from 2. until probed RMS stops converging 18 | 6. Pick best probing(lowest RMS) from "Probing history" and Apply & Save it to your machine. 19 | 20 | # Tips 21 | 22 | * Start by running with the simulated printer first to see how things work, it won't send anything to your machine. 23 | * A repeatable probe is essential. 24 | * Repeatable end stops are very important, stepper phase homing is ideal. (I've [recently added that to Marlin](https://github.com/MarlinFirmware/Marlin/pull/17299), look for TMC_HOME_PHASE) 25 | * Precision of measurements is also important, Marlin's default 2 fractional digits are not enough, use SERIAL_FLOAT_PRECISION >=4 (Also [recently added to Marlin](https://github.com/MarlinFirmware/Marlin/pull/18367) bugfix branch) 26 | * The bed must be very flat, borosilicate, or aluminum with fused pcb, or pcb should work fine, magnet mats and textured covers may introduce too much fluctuation. 27 | * Tune down the max acceleration for best repeatability. 28 | * Some geometry factors may be dependent or correlated so they can't be calibrated together, the ui will try to enforce what is possible. 29 | * Not all firmware flavors support individual adjustment for arm lengths and tower radius, some don't support automated setting of them and need to recompile. (M665 ABC [recently added to Marlin](https://github.com/MarlinFirmware/Marlin/pull/18423) bugfix branch.) 30 | * More points will make calibration to offset probe noise and converge faster, 7 is too little, 50 is a good number, 500 things start to get slow, 5000 will take a while to complete and not be much better than 500. 31 | * When probing a flat target, steps per unit errors are ambiguous to radius and arm length. I suggest you use a machinist dial to calibrate steps per unit then calibrate the other geometry factors. 32 | * Belts significantly stretch and shrink with room temperature and humidity fluctuations, radius and arms lentgth won't change significantly, so if you see a drift on your bed flatness you can calibrate steps per unit daily without touching factors you know didn't change. 33 | * This pugin is quite complex and use a non trivial ammount of resources on your browser, therefore I suggest disabling it when not in use(normal day to day printing). -------------------------------------------------------------------------------- /octoprint_DuCalibrator/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import 3 | 4 | ### (Don't forget to remove me) 5 | # This is a basic skeleton for your plugin's __init__.py. You probably want to adjust the class name of your plugin 6 | # as well as the plugin mixins it's subclassing from. This is really just a basic skeleton to get you started, 7 | # defining your plugin as a template plugin, settings and asset plugin. Feel free to add or remove mixins 8 | # as necessary. 9 | # 10 | # Take a look at the documentation on what other plugin mixins are available. 11 | 12 | import octoprint.plugin 13 | import sys 14 | 15 | class DuCalibratorPlugin(octoprint.plugin.SettingsPlugin, 16 | octoprint.plugin.AssetPlugin, 17 | octoprint.plugin.TemplatePlugin, 18 | octoprint.plugin.SimpleApiPlugin): 19 | 20 | ##~~ SettingsPlugin mixin 21 | 22 | def get_settings_defaults(self): 23 | return dict( 24 | # put your plugin's default settings here 25 | Firmware="Simulated", 26 | InitCommands="G28 ;home\n;M204 T200 ;accel\n;G0 F12000 ;speed", 27 | SafeHeight="5" 28 | ) 29 | 30 | ##~~ AssetPlugin mixin 31 | def get_assets(self): 32 | # Define your plugin's asset files to automatically include in the 33 | # core UI here. 34 | return dict( 35 | js=["js/DuCalCommon.js","js/DuCalViewModel.js","js/DuCalGeometry.js", "js/DuCalMachine.js", "js/three.js","js/TrackballControls.js", "js/trilateration.js"], 36 | css=["css/DuCalibrator.css"], 37 | less=["less/DuCalibrator.less"] 38 | ) 39 | 40 | ##~~ Softwareupdate hook 41 | 42 | def get_update_information(self): 43 | # Define the configuration for your plugin to use with the Software Update 44 | # Plugin here. See https://github.com/foosel/OctoPrint/wiki/Plugin:-Software-Update 45 | # for details. 46 | return dict( 47 | DuCalibrator=dict( 48 | displayName="Delta Micro Calibrator Plugin", 49 | displayVersion=self._plugin_version, 50 | 51 | # version check: github repository 52 | type="github_release", 53 | user="Fabi0San", 54 | repo="DuCalibrator", 55 | current=self._plugin_version, 56 | 57 | # update method: pip 58 | pip="https://github.com/Fabi0San/DuCalibrator/archive/{target_version}.zip" 59 | ) 60 | ) 61 | 62 | # If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py 63 | # ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that 64 | # can be overwritten via __plugin_xyz__ control properties. See the documentation for that. 65 | __plugin_name__ = "ΔµCalibrator" if sys.version_info[0] >= 3 else "DuCalibrator" 66 | __plugin_pythoncompat__ = ">=2.7,<4" 67 | 68 | def __plugin_load__(): 69 | global __plugin_implementation__ 70 | __plugin_implementation__ = DuCalibratorPlugin() 71 | global __plugin_hooks__ 72 | __plugin_hooks__ = { 73 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information 74 | } 75 | 76 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/js/DuCalCommon.js: -------------------------------------------------------------------------------- 1 | // common classes and utilities. 2 | 3 | const XAxis = 0; 4 | const YAxis = 1; 5 | const ZAxis = 2; 6 | const AlphaTower = 0; 7 | const BetaTower = 1; 8 | const GammaTower = 2; 9 | 10 | class ProbingData{ 11 | constructor(observable = ko.observable()) 12 | { 13 | this.DataPoints = []; 14 | this.Max = undefined; 15 | this.Min = undefined; 16 | this.RMS = undefined; 17 | this.Observable = observable; 18 | this.sumOfSquares = 0; 19 | this.Observable(this); 20 | } 21 | 22 | AddPoint(probe) 23 | { 24 | this.DataPoints.push(probe); 25 | 26 | if (this.Max === undefined || probe.Error > this.Max) 27 | this.Max = probe.Error; 28 | 29 | if (this.Min === undefined || probe.Error < this.Min) 30 | this.Min = probe.Error; 31 | 32 | this.sumOfSquares += probe.Error * probe.Error; 33 | 34 | this.RMS = Math.sqrt(this.sumOfSquares / this.DataPoints.length); 35 | 36 | this.Observable(this); 37 | } 38 | } 39 | 40 | class ProbePoint 41 | { 42 | constructor(target, actual) 43 | { 44 | this.Target = target; 45 | this.Actual = actual; 46 | } 47 | 48 | get X() 49 | { 50 | return this.Actual[XAxis]; 51 | } 52 | 53 | get Y() 54 | { 55 | return this.Actual[YAxis]; 56 | } 57 | 58 | get Z() 59 | { 60 | return this.Actual[ZAxis]; 61 | } 62 | 63 | get DeltaVector() 64 | { 65 | return [ 66 | this.Actual[XAxis] - this.Target[XAxis], 67 | this.Actual[YAxis] - this.Target[YAxis], 68 | this.Actual[ZAxis] - this.Target[ZAxis] 69 | ]; 70 | } 71 | 72 | get DeltaMagnitude() 73 | { 74 | var dv = this.DeltaVector; 75 | return Math.sqrt( 76 | Math.pow(dv[XAxis],2) + 77 | Math.pow(dv[YAxis],2) + 78 | Math.pow(dv[ZAxis],2)); 79 | } 80 | 81 | get Error() 82 | { 83 | return this.DeltaVector[ZAxis]; 84 | } 85 | } 86 | 87 | class CollapseControl { 88 | constructor() { 89 | this.Visible = ko.observable(false); 90 | } 91 | 92 | get IsHidden() 93 | { 94 | return !this.Visible(); 95 | } 96 | 97 | Hide() { 98 | this.Visible(false); 99 | } 100 | 101 | Show() { 102 | this.Visible(true); 103 | } 104 | } 105 | 106 | class DuCalUtils 107 | { 108 | static GetSpiralPoints(n, radius) { 109 | var a = radius / (2 * Math.sqrt(n * Math.PI)); 110 | var step_length = radius * radius / (2 * a * n); 111 | 112 | var result = new Array(n); 113 | 114 | for (var i = 0; i < n; i++) { 115 | var angle = Math.sqrt( 2 * (i * step_length) / a); 116 | var r = angle * a; 117 | 118 | // polar to cartesian 119 | var x = r * Math.cos(angle); 120 | var y = r * Math.sin(angle); 121 | result[i] = [x, y]; 122 | } 123 | return result; 124 | } 125 | 126 | static Normalize(arr) 127 | { 128 | var factor = Math.min.apply(null, arr); 129 | for(var i = 0 ; i 2 |

3 | 4 | 5 | {{ _('Current Geometry') }} 6 | 7 |

8 |
9 |
10 |
11 |
Parameter
12 |
13 |
14 |
Value
15 |
16 |
17 | 18 |
19 |
{{ _('Steps per unit') }}
20 |
21 |
22 |
23 |
24 | 25 |
26 |
{{ _('Tower Offset') }}
27 |
28 |
29 |
30 |
31 | 32 |
33 |
{{ _('Max Height') }}
34 |
35 |
36 | 37 |
38 |
{{ _('Endstop Offset') }}
39 |
40 |
41 |
42 |
43 | 44 |
45 |
{{ _('Rod Length') }}
46 |
47 |
48 | 49 |
50 |
{{ _('Rod Length Adjust') }}
51 |
52 |
53 |
54 |
55 | 56 |
57 |
{{ _('Delta Radius') }}
58 |
59 |
60 | 61 |
62 |
{{ _('Delta Radius Adjust') }}
63 |
64 |
65 |
66 |
67 |
68 | 69 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/js/trilateration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript implementation of Trilateration to find the position of a 3 | * point (P4) from three known points in 3D space (P1, P2, P3) and their 4 | * distance from the point in question. 5 | * 6 | * The solution used here is based on the derivation found on the Wikipedia 7 | * page of Trilateration: https://en.wikipedia.org/wiki/Trilateration 8 | * 9 | * This library does not need any 3rd party tools as all the non-basic 10 | * geometric functions needed are declared inside the trilaterate() function. 11 | * 12 | * See the GitHub page: https://github.com/gheja/trilateration.js 13 | */ 14 | 15 | /** 16 | * Calculates the coordinates of a point in 3D space from three known points 17 | * and the distances between those points and the point in question. 18 | * 19 | * If no solution found then null will be returned. 20 | * 21 | * If two solutions found then both will be returned, unless the fourth 22 | * parameter (return_middle) is set to true when the middle of the two solution 23 | * will be returned. 24 | * 25 | * @param {Object} p1 Point and distance: { x, y, z, r } 26 | * @param {Object} p2 Point and distance: { x, y, z, r } 27 | * @param {Object} p3 Point and distance: { x, y, z, r } 28 | * @param {bool} return_middle If two solution found then return the center of them 29 | * @return {Object|Array|null} { x, y, z } or [ { x, y, z }, { x, y, z } ] or null 30 | */ 31 | function trilaterate(p1, p2, p3, return_middle) 32 | { 33 | // based on: https://en.wikipedia.org/wiki/Trilateration 34 | 35 | // some additional local functions declared here for 36 | // scalar and vector operations 37 | 38 | function sqr(a) 39 | { 40 | return a * a; 41 | } 42 | 43 | function norm(a) 44 | { 45 | return Math.sqrt(sqr(a.x) + sqr(a.y) + sqr(a.z)); 46 | } 47 | 48 | function dot(a, b) 49 | { 50 | return a.x * b.x + a.y * b.y + a.z * b.z; 51 | } 52 | 53 | function vector_subtract(a, b) 54 | { 55 | return { 56 | x: a.x - b.x, 57 | y: a.y - b.y, 58 | z: a.z - b.z 59 | }; 60 | } 61 | 62 | function vector_add(a, b) 63 | { 64 | return { 65 | x: a.x + b.x, 66 | y: a.y + b.y, 67 | z: a.z + b.z 68 | }; 69 | } 70 | 71 | function vector_divide(a, b) 72 | { 73 | return { 74 | x: a.x / b, 75 | y: a.y / b, 76 | z: a.z / b 77 | }; 78 | } 79 | 80 | function vector_multiply(a, b) 81 | { 82 | return { 83 | x: a.x * b, 84 | y: a.y * b, 85 | z: a.z * b 86 | }; 87 | } 88 | 89 | function vector_cross(a, b) 90 | { 91 | return { 92 | x: a.y * b.z - a.z * b.y, 93 | y: a.z * b.x - a.x * b.z, 94 | z: a.x * b.y - a.y * b.x 95 | }; 96 | } 97 | 98 | var ex, ey, ez, i, j, d, a, x, y, z, b, p4; 99 | 100 | ex = vector_divide(vector_subtract(p2, p1), norm(vector_subtract(p2, p1))); 101 | 102 | i = dot(ex, vector_subtract(p3, p1)); 103 | a = vector_subtract(vector_subtract(p3, p1), vector_multiply(ex, i)); 104 | ey = vector_divide(a, norm(a)); 105 | ez = vector_cross(ex, ey); 106 | d = norm(vector_subtract(p2, p1)); 107 | j = dot(ey, vector_subtract(p3, p1)); 108 | 109 | x = (sqr(p1.r) - sqr(p2.r) + sqr(d)) / (2 * d); 110 | y = (sqr(p1.r) - sqr(p3.r) + sqr(i) + sqr(j)) / (2 * j) - (i / j) * x; 111 | 112 | b = sqr(p1.r) - sqr(x) - sqr(y); 113 | 114 | // floating point math flaw in IEEE 754 standard 115 | // see https://github.com/gheja/trilateration.js/issues/2 116 | if (Math.abs(b) < 0.0000000001) 117 | { 118 | b = 0; 119 | } 120 | 121 | z = Math.sqrt(b); 122 | 123 | // no solution found 124 | if (isNaN(z)) 125 | { 126 | return null; 127 | } 128 | 129 | a = vector_add(p1, vector_add(vector_multiply(ex, x), vector_multiply(ey, y))) 130 | p4a = vector_add(a, vector_multiply(ez, z)); 131 | p4b = vector_subtract(a, vector_multiply(ez, z)); 132 | 133 | return [ p4a, p4b ]; 134 | } 135 | 136 | //module.exports = trilaterate; 137 | -------------------------------------------------------------------------------- /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 = "DuCalibrator" 8 | 9 | # The plugin's python package, should be "octoprint_", has to be unique 10 | plugin_package = "octoprint_DuCalibrator" 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 = "Delta Micro Calibrator" 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.0.4" 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 = """An OctoPrint Plugin for performing micro calibration in linear delta machines.""" 22 | 23 | # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module 24 | plugin_author = "Fabio Santos" 25 | 26 | # The plugin's author's mail address. 27 | plugin_author_email = "fabiosan@live.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/Fabi0San/DuCalibrator" 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 = [] 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 | # Any additional python packages you need to install with your plugin that are not contained in .* 49 | plugin_additional_packages = [] 50 | 51 | # Any python packages within .* you do NOT want to install with your plugin 52 | plugin_ignored_packages = [] 53 | 54 | # Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, 55 | # define dependency links or other things like that, this is the place to go. Will be merged recursively with the 56 | # default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using 57 | # octoprint.util.dict_merge. 58 | # 59 | # Example: 60 | # plugin_requires = ["someDependency==dev"] 61 | # additional_setup_parameters = {"dependency_links": ["https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} 62 | additional_setup_parameters = {} 63 | 64 | ######################################################################################################################## 65 | 66 | from setuptools import setup 67 | 68 | try: 69 | import octoprint_setuptools 70 | except: 71 | print("Could not import OctoPrint's setuptools, are you sure you are running that under " 72 | "the same python installation that OctoPrint is installed under?") 73 | import sys 74 | sys.exit(-1) 75 | 76 | setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( 77 | identifier=plugin_identifier, 78 | package=plugin_package, 79 | name=plugin_name, 80 | version=plugin_version, 81 | description=plugin_description, 82 | author=plugin_author, 83 | mail=plugin_author_email, 84 | url=plugin_url, 85 | license=plugin_license, 86 | requires=plugin_requires, 87 | additional_packages=plugin_additional_packages, 88 | ignored_packages=plugin_ignored_packages, 89 | additional_data=plugin_additional_data 90 | ) 91 | 92 | if len(additional_setup_parameters): 93 | from octoprint.util import dict_merge 94 | setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) 95 | 96 | setup(**setup_parameters) 97 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/templates/Calibration.jinja2: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 |
12 |

13 | 14 | 15 | {{ _('Calibration') }} 16 | 17 |

18 |
19 |
20 |
21 |
Parameter
22 |
23 |
24 | 28 |
29 |
30 |
Calibrate
31 |
32 |
33 | 34 |
35 |
{{ _('Steps per unit') }}
36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 |
{{ _('Tower Offset') }}
44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 |
{{ _('Max Height') }}
52 | 53 |
54 | 55 |
56 |
57 | 58 |
59 |
{{ _('Endstop Offset') }}
60 | 61 |
62 | 63 |
64 |
65 | 66 |
67 |
{{ _('Rod Length') }}
68 | 69 |
70 | 71 |
72 |
73 | 74 |
75 |
{{ _('Rod Length Adjust') }}
76 | 77 |
78 | 79 |
80 |
81 | 82 |
83 |
{{ _('Delta Radius') }}
84 | 85 |
86 | 87 |
88 |
89 | 90 |
91 |
{{ _('Delta Radius Adjust') }}
92 | 93 |
94 | 95 |
96 |
97 |
98 |
99 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/js/DuCalGeometry.js: -------------------------------------------------------------------------------- 1 | // Delta calibration script 2 | const degreesToRadians = Math.PI / 180.0; 3 | const MaxFactors = 17; 4 | const AllTowers = [AlphaTower, BetaTower, GammaTower]; 5 | 6 | class Matrix 7 | { 8 | constructor(rows, cols) { 9 | this.data = new Array(rows); 10 | for (var i = 0; i < rows; ++i) 11 | this.data[i] = (new Array(cols)).fill(0.0); 12 | } 13 | 14 | SwapRows(i, j, numCols) { 15 | if (i !== j) { 16 | for (var k = 0; k < numCols; ++k) { 17 | var temp = this.data[i][k]; 18 | this.data[i][k] = this.data[j][k]; 19 | this.data[j][k] = temp; 20 | } 21 | } 22 | } 23 | 24 | // Perform Gauus-Jordan elimination on a matrix with numRows rows and (njumRows + 1) columns 25 | GaussJordan(solution, numRows) { 26 | for (var i = 0; i < numRows; ++i) { 27 | // Swap the rows around for stable Gauss-Jordan elimination 28 | var vmax = Math.abs(this.data[i][i]); 29 | for (var j = i + 1; j < numRows; ++j) { 30 | var rmax = Math.abs(this.data[j][i]); 31 | if (rmax > vmax) { 32 | this.SwapRows(i, j, numRows + 1); 33 | vmax = rmax; 34 | } 35 | } 36 | 37 | // Use row i to eliminate the ith element from previous and subsequent rows 38 | var v = this.data[i][i]; 39 | for (var j = 0; j < i; ++j) { 40 | var factor = this.data[j][i]/v; 41 | this.data[j][i] = 0.0; 42 | for (var k = i + 1; k <= numRows; ++k) { 43 | this.data[j][k] -= this.data[i][k] * factor; 44 | } 45 | } 46 | 47 | for (var j = i + 1; j < numRows; ++j) { 48 | var factor = this.data[j][i]/v; 49 | this.data[j][i] = 0.0; 50 | for (var k = i + 1; k <= numRows; ++k) { 51 | this.data[j][k] -= this.data[i][k] * factor; 52 | } 53 | } 54 | } 55 | 56 | for (var i = 0; i < numRows; ++i) { 57 | solution.push(this.data[i][numRows] / this.data[i][i]); 58 | } 59 | } 60 | } 61 | 62 | class DeltaGeometry 63 | { 64 | constructor(diagonalRod = 0, radius = 0, height = 0, endStopOffset = [0,0,0], towerOffset = [0,0,0], stepsPerUnit = [1,1,1], radiusAdjust = [0,0,0], diagonalRodAdjust = [0,0,0]) 65 | { 66 | this.DiagonalRod = diagonalRod; 67 | this.DiagonalRodAdjust = diagonalRodAdjust.slice(); 68 | this.Radius = radius; 69 | this.RadiusAdjust = radiusAdjust.slice(); 70 | this.TowerOffset = towerOffset.slice(); 71 | this.StepsPerUnit = stepsPerUnit.slice(); 72 | this.Height = height; 73 | this.EndStopOffset = endStopOffset.slice(); 74 | 75 | this.RecomputeGeometry(); 76 | } 77 | 78 | 79 | Clone() { 80 | return new DeltaGeometry(this.DiagonalRod, this.Radius, this.Height, this.EndStopOffset, this.TowerOffset, this.StepsPerUnit, this.RadiusAdjust, this.DiagonalRodAdjust); 81 | } 82 | 83 | RecomputeGeometry() { 84 | this.towerPositions = [ 85 | [-((this.Radius + this.RadiusAdjust[AlphaTower]) * Math.cos((30 + this.TowerOffset[AlphaTower]) * degreesToRadians)), 86 | -((this.Radius + this.RadiusAdjust[AlphaTower]) * Math.sin((30 + this.TowerOffset[AlphaTower]) * degreesToRadians))], 87 | [+((this.Radius + this.RadiusAdjust[BetaTower]) * Math.cos((30 - this.TowerOffset[BetaTower]) * degreesToRadians)), 88 | -((this.Radius + this.RadiusAdjust[BetaTower]) * Math.sin((30 - this.TowerOffset[BetaTower]) * degreesToRadians))], 89 | [-((this.Radius + this.RadiusAdjust[GammaTower]) * Math.sin(this.TowerOffset[GammaTower] * degreesToRadians)), 90 | +((this.Radius + this.RadiusAdjust[GammaTower]) * Math.cos(this.TowerOffset[GammaTower] * degreesToRadians))]]; 91 | 92 | 93 | // compute tower height 94 | this.TowerHeight = AllTowers.map(tower => 95 | (this.EndStopOffset[tower] + // height from endstop to home position in steps 96 | this.Height + // height from home to carriage at touch in steps 97 | this.CarriagemmFromBottom([0, 0, 0], tower))); // height from carriage at touch to bed in mm 98 | } 99 | 100 | GetCarriagePosition(position) { 101 | return AllTowers.map(tower => //Math.round // rounded to constrain to machine's adressable positions 102 | (this.TowerHeight[tower] 103 | - this.CarriagemmFromBottom(position, tower)) 104 | * this.StepsPerUnit[tower]); // fromBottom 105 | } 106 | 107 | CarriagemmFromBottom(machinePos, tower) 108 | { 109 | return machinePos[ZAxis] + Math.sqrt(Math.pow(this.DiagonalRod + this.DiagonalRodAdjust[tower],2) - Math.pow(machinePos[XAxis] - this.towerPositions[tower][XAxis],2) - Math.pow(machinePos[YAxis] - this.towerPositions[tower][YAxis],2)); 110 | } 111 | 112 | GetEffectorPosition(carriagePositions) { 113 | var p = AllTowers.map(tower => ({ 114 | x: (this.towerPositions[tower][XAxis]), 115 | y: (this.towerPositions[tower][YAxis]), 116 | z: (this.TowerHeight[tower] - (carriagePositions[tower] / this.StepsPerUnit[tower])), 117 | r: this.DiagonalRod + this.DiagonalRodAdjust[tower] 118 | })); 119 | var results = trilaterate(p[0], p[1], p[2]); 120 | return [results[1].x, results[1].y, results[1].z]; 121 | } 122 | 123 | ComputeDerivative(factor, carriagePositions, target) 124 | { 125 | var perturb = 0.2; 126 | var hiParams = this.Clone(); 127 | var loParams = this.Clone(); 128 | var adjust = Array(MaxFactors).fill(0.0); 129 | var factorMap = Array(MaxFactors).fill(true); 130 | 131 | adjust[factor] = perturb; 132 | hiParams.Adjust(factorMap, adjust); 133 | 134 | adjust[factor] = -perturb; 135 | loParams.Adjust(factorMap, adjust); 136 | 137 | var posHi = hiParams.GetEffectorPosition(carriagePositions); 138 | var posLo = loParams.GetEffectorPosition(carriagePositions); 139 | 140 | var probeHi = new ProbePoint(target, posHi); 141 | var probeLo = new ProbePoint(target, posLo); 142 | 143 | return (probeHi.Error - probeLo.Error) / (2 * perturb); 144 | } 145 | 146 | Adjust(factors, corrections) 147 | { 148 | var i = 0; 149 | 150 | if (factors[0]) this.EndStopOffset[AlphaTower] += corrections[i++]; 151 | if (factors[1]) this.EndStopOffset[BetaTower] += corrections[i++]; 152 | if (factors[2]) this.EndStopOffset[GammaTower] += corrections[i++]; 153 | if (factors[3]) this.Radius += corrections[i++]; 154 | if (factors[4]) this.TowerOffset[AlphaTower] += corrections[i++]; 155 | if (factors[5]) this.TowerOffset[BetaTower] += corrections[i++]; 156 | if (factors[6]) this.DiagonalRod += corrections[i++]; 157 | if (factors[7]) this.StepsPerUnit[AlphaTower] += corrections[i++]; 158 | if (factors[8]) this.StepsPerUnit[BetaTower] += corrections[i++]; 159 | if (factors[9]) this.StepsPerUnit[GammaTower] += corrections[i++]; 160 | if (factors[10]) this.RadiusAdjust[AlphaTower] += corrections[i++]; 161 | if (factors[11]) this.RadiusAdjust[BetaTower] += corrections[i++]; 162 | if (factors[12]) this.RadiusAdjust[GammaTower] += corrections[i++]; 163 | if (factors[13]) this.DiagonalRodAdjust[AlphaTower] += corrections[i++]; 164 | if (factors[14]) this.DiagonalRodAdjust[BetaTower] += corrections[i++]; 165 | if (factors[15]) this.DiagonalRodAdjust[GammaTower] += corrections[i++]; 166 | if (factors[16]) this.Height += corrections[i++]; 167 | 168 | // normalize factors with 3 adjusts 169 | var endStopOffsets = this.EndStopOffset; 170 | this.Height += DuCalUtils.Normalize(endStopOffsets); 171 | this.EndStopOffset = endStopOffsets; 172 | 173 | this.DiagonalRod += DuCalUtils.Normalize(this.DiagonalRodAdjust); 174 | this.Radius += DuCalUtils.Normalize(this.RadiusAdjust); 175 | DuCalUtils.Normalize(this.TowerOffset); 176 | 177 | this.RecomputeGeometry(); 178 | } 179 | 180 | static Calibrate(currentGeometry, probeData, factors) 181 | { 182 | var debug = false; 183 | var numFactors = 0; 184 | for (var i = 0; i < MaxFactors; i++) 185 | if (factors[i]) 186 | numFactors++; 187 | 188 | var numPoints = probeData.DataPoints.length; 189 | 190 | if (numFactors > numPoints) { 191 | throw "Error: need at least as many points as factors you want to calibrate"; 192 | } 193 | 194 | // Transform the probing points to motor endpoints and store them in a matrix, so that we can do multiple iterations using the same data 195 | var probedCarriagePositions = probeData.DataPoints.map(point => currentGeometry.GetCarriagePosition(point.Actual)); 196 | var corrections = new Array(numPoints).fill(0.0); 197 | var initialSumOfSquares = probeData.DataPoints.reduce((acc, val) => acc += Math.pow(val.Error, 2), 0.0); 198 | 199 | // Do 1 or more Newton-Raphson iterations 200 | var initialRms = Math.sqrt(initialSumOfSquares / numPoints); 201 | var previousRms = initialRms; 202 | var expectedRmsError; 203 | var bestRmsError = initialRms; 204 | var bestGeometry = currentGeometry.Clone(); 205 | var bestResiduals = probeData.DataPoints; 206 | for (var iteration = 0; iteration<20; iteration++) { 207 | // Build a matrix of derivatives. 208 | var derivativeMatrix = new Matrix(numPoints, numFactors); 209 | for (var i = 0; i < numPoints; ++i) { 210 | var j = 0; 211 | for (var k = 0; k < MaxFactors; k++) { 212 | if (factors[k]) { 213 | derivativeMatrix.data[i][j++] = 214 | currentGeometry.ComputeDerivative(k, probedCarriagePositions[i], probeData.DataPoints[i].Target); 215 | } 216 | } 217 | } 218 | 219 | //console.log(derivativeMatrix); 220 | 221 | // Now build the normal equations for least squares fitting 222 | var normalMatrix = new Matrix(numFactors, numFactors + 1); 223 | for (var i = 0; i < numFactors; ++i) { 224 | for (var j = 0; j < numFactors; ++j) { 225 | var temp = 0; 226 | for (var k = 0; k < numPoints; ++k) { 227 | temp += derivativeMatrix.data[k][i] * derivativeMatrix.data[k][j]; 228 | } 229 | normalMatrix.data[i][j] = temp; 230 | } 231 | var temp = 0; 232 | for (var k = 0; k < numPoints; ++k) { 233 | temp += derivativeMatrix.data[k][i] * -(probeData.DataPoints[k].Error + corrections[k]); 234 | } 235 | normalMatrix.data[i][numFactors] = temp; 236 | } 237 | 238 | var solution = []; 239 | normalMatrix.GaussJordan(solution, numFactors); 240 | 241 | for (var i = 0; i < numFactors; ++i) { 242 | if (isNaN(solution[i])) { 243 | throw "Unable to calculate corrections. Please make sure the bed probe points are all distinct."; 244 | } 245 | } 246 | 247 | if (debug) { 248 | // Calculate and display the residuals 249 | var residuals = []; 250 | for (var i = 0; i < numPoints; ++i) { 251 | var r = probeData.DataPoints[i].Error; 252 | for (var j = 0; j < numFactors; ++j) { 253 | r += solution[j] * derivativeMatrix.data[i][j]; 254 | } 255 | residuals.push(r); 256 | } 257 | } 258 | 259 | currentGeometry.Adjust(factors, solution); 260 | 261 | // Calculate the expected probe heights using the new parameters 262 | { 263 | var expectedResiduals = new Array(numPoints); 264 | var sumOfSquares = 0.0; 265 | for (var i = 0; i < numPoints; ++i) { 266 | var effector = currentGeometry.GetEffectorPosition(probedCarriagePositions[i]); 267 | var newProbe = new ProbePoint(probeData.DataPoints[i].Target, effector); 268 | var correction = newProbe.Error - probeData.DataPoints[i].Error; 269 | corrections[i] = correction; 270 | expectedResiduals[i] = newProbe; 271 | sumOfSquares += Math.pow(newProbe.Error, 2); 272 | } 273 | 274 | expectedRmsError = Math.sqrt(sumOfSquares/numPoints); 275 | //console.log("Iteration " + iteration + " delta rms " + (expectedRmsError < previousRms ? "-" : "+") + Math.log10(Math.abs(expectedRmsError - previousRms)) + " improvement on initial " + (expectedRmsError - initialRms)); 276 | previousRms = expectedRmsError; 277 | } 278 | 279 | if (expectedRmsError < bestRmsError) { 280 | bestRmsError = expectedRmsError; 281 | bestGeometry = currentGeometry.Clone(); 282 | bestResiduals = expectedResiduals; 283 | iteration = 0; 284 | } 285 | } 286 | console.log("Calibrated " + numFactors + " factors using " + numPoints + " points, deviation before " + Math.sqrt(initialSumOfSquares / numPoints) + " after " + bestRmsError); 287 | 288 | return { 289 | Geometry: bestGeometry, 290 | RMS: bestRmsError, 291 | Min: Math.min.apply(null, bestResiduals?.map(i=>i.Error)), 292 | Max: Math.max.apply(null, bestResiduals?.map(i=>i.Error)), 293 | Residuals: bestResiduals 294 | }; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/js/DuCalMachine.js: -------------------------------------------------------------------------------- 1 | // Class abstracting the machine being calibrated 2 | class AbstractMachine 3 | { 4 | constructor(settings) 5 | { 6 | this.settings = settings; 7 | 8 | this.IsReady = ko.observable(false); 9 | this.IsBusy = ko.observable(false); 10 | this.Geometry = ko.observable(undefined); 11 | 12 | } 13 | 14 | ParseData(data) 15 | { 16 | } 17 | 18 | async Init() 19 | { 20 | } 21 | 22 | async GetGeometry() 23 | { 24 | } 25 | 26 | async SetGeometry(geometry, save) 27 | { 28 | } 29 | 30 | async ProbeBed(x, y) 31 | { 32 | } 33 | } 34 | 35 | class RealMachine extends AbstractMachine 36 | { 37 | constructor(settings) 38 | { 39 | super(settings); 40 | this.PopulateCommands(); 41 | this.comms = new AsyncRequestor(req => OctoPrint.control.sendGcode(req), this.commands.Echo); 42 | this.BuildGeometryParsers(); 43 | } 44 | 45 | ParseData(data) 46 | { 47 | this.comms.ReceiveResponse(data.logs); 48 | this.IsReady(data.state.flags.ready); 49 | } 50 | 51 | async Init() 52 | { 53 | await this.comms.Execute(this.commands.Init); 54 | } 55 | 56 | async GetGeometry() 57 | { 58 | this.IsBusy(true); 59 | 60 | if(!this.Geometry()) 61 | { 62 | await this.Init(); 63 | } 64 | 65 | const response = await this.comms.Execute(this.commands.FetchSettings); 66 | 67 | var newGeometry = this.Geometry() ?? new DeltaGeometry() ; 68 | 69 | for (var i = 0; i < response.length; i++) { 70 | this.geometryElementParsers.forEach(element => element.ParseLog(newGeometry, response[i])); 71 | } 72 | 73 | this.Geometry(newGeometry); 74 | this.IsBusy(false); 75 | return newGeometry; 76 | } 77 | 78 | async SetGeometry(geometry, save) 79 | { 80 | this.IsBusy(true); 81 | await this.comms.Execute(this.geometryElementParsers.map(element => element.GetCommand(geometry))); 82 | if(save) 83 | await this.comms.Execute(this.commands.SaveSettings); 84 | await this.Init(); 85 | const result = await this.GetGeometry(); 86 | this.IsBusy(false); 87 | return result; 88 | } 89 | } 90 | 91 | class MarlinMachine extends RealMachine 92 | { 93 | constructor(settings) 94 | { 95 | super(settings); 96 | } 97 | 98 | BuildGeometryParsers() 99 | { 100 | this.geometryElementParsers = [ 101 | new GeometryElementParser(this.commands.StepsPerUnit, this.commands.idsStepsPerUnit, (geometry, value) => geometry.StepsPerUnit = value, (geometry) => geometry.StepsPerUnit), 102 | 103 | // endstop offset is added to the height, marlin reverses this for some reason. 104 | new GeometryElementParser(this.commands.EndStopOffset, this.commands.idsEndStopOffset, (geometry, value) => geometry.EndStopOffset = value.map(i=>-i), (geometry) => geometry.EndStopOffset.map(i=>-i)), 105 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsTowerAngleOffset, (geometry, value) => geometry.TowerOffset = value, (geometry) => geometry.TowerOffset), 106 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.DiagonalRodAdjust, (geometry, value) => geometry.DiagonalRodAdjust = value, (geometry) => geometry.DiagonalRodAdjust), 107 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusHeightRod[0], (geometry, value) => geometry.Radius = value, (geometry) => geometry.Radius), 108 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusHeightRod[1], (geometry, value) => geometry.Height = value, (geometry) => geometry.Height), 109 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusHeightRod[2], (geometry, value) => geometry.DiagonalRod = value, (geometry) => geometry.DiagonalRod), 110 | ]; 111 | } 112 | 113 | PopulateCommands() 114 | { 115 | this.commands = 116 | { 117 | Init: this.settings.InitCommands().split("\n"), 118 | Echo: "M118", 119 | Move: "G0", 120 | ProbeBed: "G30", 121 | FetchSettings: "M503", 122 | SaveSettings: "M500", 123 | StepsPerUnit: "M92", 124 | EndStopOffset: "M666", 125 | DeltaConfig: "M665", 126 | idsRadiusHeightRod: "RHL", 127 | idsTowerAngleOffset: "XYZ", 128 | idsEndStopOffset: "XYZ", 129 | idsStepsPerUnit: "XYZ", 130 | DiagonalRodAdjust: "ABC", 131 | } 132 | } 133 | 134 | async ProbeBed(x, y, retract) 135 | { 136 | this.IsBusy(true); 137 | 138 | const commands = [ 139 | `${this.commands.Move} Z${this.settings.SafeHeight()}`, // safe height 140 | `${this.commands.Move} X${x.toFixed(5)} Y${y.toFixed(5)}`, // position 141 | `${this.commands.ProbeBed} E${retract ? 1 : 0}` // probe 142 | ]; 143 | 144 | const response = await this.comms.Execute(commands); 145 | 146 | const probePointRegex = /Bed X: (-?\d+\.?\d*) Y: (-?\d+\.?\d*) Z: (-?\d+\.?\d*)/; 147 | var match; 148 | var result = undefined; 149 | 150 | for (var i = 0; i < response.length; i++) 151 | { 152 | if (match = probePointRegex.exec(response[i])) 153 | { 154 | result = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]; 155 | break; 156 | } 157 | } 158 | 159 | this.IsBusy(false); 160 | return result; 161 | } 162 | } 163 | 164 | class SmoothieMachine extends RealMachine 165 | { 166 | constructor(settings) 167 | { 168 | super(settings); 169 | } 170 | 171 | BuildGeometryParsers() 172 | { 173 | this.geometryElementParsers = [ 174 | new GeometryElementParser(this.commands.StepsPerUnit, this.commands.idsStepsPerUnit, (geometry, value) => geometry.StepsPerUnit = value, (geometry) => geometry.StepsPerUnit), 175 | new GeometryElementParser(this.commands.EndStopOffset, this.commands.idsEndStopOffset, (geometry, value) => geometry.EndStopOffset = value.map(i=>i), (geometry) => geometry.EndStopOffset.map(i=>i)), 176 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsTowerAngleOffset, (geometry, value) => geometry.TowerOffset = value, (geometry) => geometry.TowerOffset), 177 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusOffset, (geometry, value) => geometry.RadiusAdjust = value, (geometry) => geometry.RadiusAdjust), 178 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusHeightRod[0], (geometry, value) => geometry.Radius = value, (geometry) => geometry.Radius), 179 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusHeightRod[1], (geometry, value) => geometry.Height = value, (geometry) => geometry.Height), 180 | new GeometryElementParser(this.commands.DeltaConfig, this.commands.idsRadiusHeightRod[2], (geometry, value) => geometry.DiagonalRod = value, (geometry) => geometry.DiagonalRod), 181 | ]; 182 | } 183 | 184 | PopulateCommands() 185 | { 186 | this.commands = 187 | { 188 | Init: this.settings.InitCommands().split("\n"), 189 | Echo: "M118", 190 | Move: "G0", 191 | ProbeBed: "G30", 192 | FetchSettings: "M503", 193 | SaveSettings: "M500", 194 | StepsPerUnit: "M92", 195 | EndStopOffset: "M666", 196 | DeltaConfig: "M665", 197 | idsRadiusHeightRod: "RZL", 198 | idsTowerAngleOffset: "DEH", 199 | idsEndStopOffset: "XYZ", 200 | idsStepsPerUnit: "XYZ", 201 | idsRadiusOffset: "ABC" 202 | } 203 | } 204 | 205 | async ProbeBed(x, y) 206 | { 207 | this.IsBusy(true); 208 | 209 | const commands = [ 210 | `${this.commands.Move} Z${this.settings.SafeHeight()}`, // safe height 211 | `${this.commands.Move} X${x.toFixed(5)} Y${y.toFixed(5)}`, // position 212 | `${this.commands.ProbeBed}` // probe 213 | ]; 214 | 215 | const response = await this.comms.Execute(commands); 216 | 217 | const probePointRegex = /Bed X: (-?\d+\.?\d*) Y: (-?\d+\.?\d*) Z: (-?\d+\.?\d*)/; 218 | var match; 219 | var result = undefined; 220 | 221 | for (var i = 0; i < response.length; i++) 222 | { 223 | if (match = probePointRegex.exec(response[i])) 224 | { 225 | result = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]; 226 | break; 227 | } 228 | } 229 | 230 | this.IsBusy(false); 231 | return result; 232 | } 233 | 234 | } 235 | 236 | class TestMachine extends AbstractMachine 237 | { 238 | constructor(settings, actualGeometry, initialGeometry) 239 | { 240 | super(settings); 241 | this.actualGeometry = actualGeometry; 242 | this.initialGeometry = initialGeometry; 243 | this.IsReady(true); 244 | } 245 | 246 | async Init() 247 | { 248 | this.Geometry(this.initialGeometry); 249 | } 250 | 251 | async GetGeometry() 252 | { 253 | if(!this.Geometry()) 254 | { 255 | await this.Init(); 256 | } 257 | 258 | return this.Geometry(); 259 | } 260 | 261 | async SetGeometry(geometry) 262 | { 263 | this.Geometry(geometry); 264 | } 265 | 266 | async ProbeBed(x,y) 267 | { 268 | const almostZero = 1e-10; 269 | var targetZ = 0; 270 | var carriagePositions; 271 | var actualPosition; 272 | 273 | await new Promise(resolve => setTimeout(resolve, 1)); 274 | 275 | // search a point on the current geometry that hit z0 on the actual geomety. 276 | do 277 | { 278 | targetZ -= actualPosition?.[ZAxis] ?? 0; 279 | carriagePositions = this.Geometry().GetCarriagePosition([x, y, targetZ]); 280 | actualPosition = this.actualGeometry.GetEffectorPosition(carriagePositions); 281 | } while (Math.abs(actualPosition[ZAxis]) > almostZero); 282 | 283 | // round it UP from assumed trigger point, tor nearest addressable point. 284 | //carriagePositions = carriagePositions.map(p=>Math.ceil(p)); 285 | 286 | return this.Geometry().GetEffectorPosition(carriagePositions); 287 | } 288 | } 289 | 290 | class GeometryElementParser { 291 | constructor(command, element, setFunction, getFunction) { 292 | this.command = command; 293 | this.element = element && element.length > 0 ? Array.from(element) : new Array(0); 294 | this.setFunction = setFunction; 295 | this.getFunction = getFunction; 296 | this.regex = this.element.map(e => new RegExp(`${command} .*${e}(-?\\d+\\.?\\d*)`)); 297 | } 298 | 299 | ParseLog(geometry, logLine) { 300 | if (this.element.length == 0) { 301 | return; 302 | } 303 | 304 | var match; 305 | 306 | if (this.element.length == 1) { 307 | if (match = this.regex[0].exec(logLine)) 308 | this.setFunction(geometry, parseFloat(match[1])); 309 | return; 310 | } 311 | 312 | var result = this.getFunction(geometry); 313 | for (let i = 0; i < this.regex.length; i++) { 314 | if (match = this.regex[i].exec(logLine)) 315 | result[i] = parseFloat(match[1]) 316 | } 317 | 318 | this.setFunction(geometry, result); 319 | } 320 | 321 | GetCommand(geometry) { 322 | if (this.element.length == 0) { 323 | return; 324 | } 325 | 326 | if (this.element.length == 1) { 327 | return `${this.command} ${this.element}${this.getFunction(geometry)}`; 328 | } 329 | 330 | var value = this.getFunction(geometry); 331 | var result = this.command; 332 | for (let i = 0; i < this.element.length; i++) { 333 | result += ` ${this.element[i]}${value[i].toFixed(5)}`; 334 | } 335 | 336 | return result; 337 | } 338 | } 339 | 340 | class AsyncRequestor { 341 | constructor(sendRequestFunction, cmdEcho) { 342 | this.requestQueue = []; 343 | this.currentRequest = null; 344 | this.sendRequestFunction = sendRequestFunction; 345 | this.lastRequestId = 0; 346 | this.cmdEcho = cmdEcho 347 | } 348 | 349 | Execute(query) 350 | { 351 | const doneString = `DONE_${this.lastRequestId++}`; 352 | return this.Query([query, `${this.cmdEcho} ${doneString}`].flat(Infinity), (str) => str.includes(`Recv: ${doneString}`)); 353 | } 354 | 355 | Query(query, isFinished, timeout) { 356 | return new Promise((resolve, reject) => this.Executor(query, isFinished, timeout, resolve, reject)); 357 | } 358 | 359 | Executor(query, isFinished, timeout, resolve, reject) { 360 | this.requestQueue.push({ query: query, isFinished: isFinished, timeout: timeout, resolve: resolve, reject: reject, response: [], timeoutHandle: null , responseWatermark: 0}); 361 | this.TryDequeue(); 362 | } 363 | 364 | TryDequeue() { 365 | if (this.currentRequest === null && this.requestQueue.length > 0) { 366 | var request = this.requestQueue.shift(); 367 | this.StartRequest(request); 368 | } 369 | } 370 | 371 | ReceiveResponse(data) { 372 | if (this.currentRequest !== null) { 373 | var request = this.currentRequest; 374 | request.response = request.response.concat(data); 375 | if (request.isFinished(data)) { 376 | this.EndRequest(); 377 | request.resolve(request.response); 378 | } 379 | } 380 | else { 381 | this.TryDequeue(); 382 | } 383 | } 384 | 385 | StartRequest(request) { 386 | this.currentRequest = request; 387 | this.sendRequestFunction(request.query); 388 | if (request.timeout) 389 | request.timeoutHandle = setTimeout(this.Error, request.timeout, this, request, "Timeout"); 390 | request.watchdogHandle = setInterval(this.Watchdog, 1000, request, this); 391 | } 392 | 393 | EndRequest() { 394 | if (this.currentRequest.timeoutHandle) 395 | clearTimeout(this.currentRequest.timeoutHandle); 396 | if(this.currentRequest.watchdogHandle) 397 | clearInterval(this.currentRequest.watchdogHandle); 398 | this.currentRequest = null; 399 | this.TryDequeue(); 400 | } 401 | 402 | Error(self, request, error) { 403 | if (self.currentRequest === request) { 404 | self.EndRequest(); 405 | request.reject(request.response + error); 406 | } 407 | } 408 | 409 | Watchdog(request, self) 410 | { 411 | // are we still waiting on our request? 412 | if (self.currentRequest === request) 413 | if(request.responseWatermark == request.response.length) 414 | self.sendRequestFunction(`${self.cmdEcho} PING`).catch((ob, err) => self.Error(self, request, err)); 415 | else request.responseWatermark = request.response.length; 416 | } 417 | 418 | } 419 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/js/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | * @author Simone Manini / http://daron1337.github.io 5 | * @author Luca Antiga / http://lantiga.github.io 6 | */ 7 | 8 | THREE.TrackballControls = function ( object, domElement ) { 9 | 10 | var _this = this; 11 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 12 | 13 | this.object = object; 14 | this.domElement = ( domElement !== undefined ) ? domElement : document; 15 | 16 | // API 17 | 18 | this.enabled = true; 19 | 20 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 21 | 22 | this.rotateSpeed = 1.0; 23 | this.zoomSpeed = 1.2; 24 | this.panSpeed = 0.3; 25 | 26 | this.noRotate = false; 27 | this.noZoom = false; 28 | this.noPan = false; 29 | 30 | this.staticMoving = false; 31 | this.dynamicDampingFactor = 0.2; 32 | 33 | this.minDistance = 0; 34 | this.maxDistance = Infinity; 35 | 36 | this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 37 | 38 | // internals 39 | 40 | this.target = new THREE.Vector3(); 41 | 42 | var EPS = 0.000001; 43 | 44 | var lastPosition = new THREE.Vector3(); 45 | 46 | var _state = STATE.NONE, 47 | _prevState = STATE.NONE, 48 | 49 | _eye = new THREE.Vector3(), 50 | 51 | _movePrev = new THREE.Vector2(), 52 | _moveCurr = new THREE.Vector2(), 53 | 54 | _lastAxis = new THREE.Vector3(), 55 | _lastAngle = 0, 56 | 57 | _zoomStart = new THREE.Vector2(), 58 | _zoomEnd = new THREE.Vector2(), 59 | 60 | _touchZoomDistanceStart = 0, 61 | _touchZoomDistanceEnd = 0, 62 | 63 | _panStart = new THREE.Vector2(), 64 | _panEnd = new THREE.Vector2(); 65 | 66 | // for reset 67 | 68 | this.target0 = this.target.clone(); 69 | this.position0 = this.object.position.clone(); 70 | this.up0 = this.object.up.clone(); 71 | 72 | // events 73 | 74 | var changeEvent = { type: 'change' }; 75 | var startEvent = { type: 'start' }; 76 | var endEvent = { type: 'end' }; 77 | 78 | 79 | // methods 80 | 81 | this.handleResize = function () { 82 | 83 | if ( this.domElement === document ) { 84 | 85 | this.screen.left = 0; 86 | this.screen.top = 0; 87 | this.screen.width = window.innerWidth; 88 | this.screen.height = window.innerHeight; 89 | 90 | } else { 91 | 92 | var box = this.domElement.getBoundingClientRect(); 93 | // adjustments come from similar code in the jquery offset() function 94 | var d = this.domElement.ownerDocument.documentElement; 95 | this.screen.left = box.left + window.pageXOffset - d.clientLeft; 96 | this.screen.top = box.top + window.pageYOffset - d.clientTop; 97 | this.screen.width = box.width; 98 | this.screen.height = box.height; 99 | 100 | } 101 | 102 | }; 103 | 104 | this.handleEvent = function ( event ) { 105 | 106 | if ( typeof this[ event.type ] == 'function' ) { 107 | 108 | this[ event.type ]( event ); 109 | 110 | } 111 | 112 | }; 113 | 114 | var getMouseOnScreen = ( function () { 115 | 116 | var vector = new THREE.Vector2(); 117 | 118 | return function getMouseOnScreen( pageX, pageY ) { 119 | 120 | vector.set( 121 | ( pageX - _this.screen.left ) / _this.screen.width, 122 | ( pageY - _this.screen.top ) / _this.screen.height 123 | ); 124 | 125 | return vector; 126 | 127 | }; 128 | 129 | }() ); 130 | 131 | var getMouseOnCircle = ( function () { 132 | 133 | var vector = new THREE.Vector2(); 134 | 135 | return function getMouseOnCircle( pageX, pageY ) { 136 | 137 | vector.set( 138 | ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), 139 | ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional 140 | ); 141 | 142 | return vector; 143 | 144 | }; 145 | 146 | }() ); 147 | 148 | this.rotateCamera = ( function() { 149 | 150 | var axis = new THREE.Vector3(), 151 | quaternion = new THREE.Quaternion(), 152 | eyeDirection = new THREE.Vector3(), 153 | objectUpDirection = new THREE.Vector3(), 154 | objectSidewaysDirection = new THREE.Vector3(), 155 | moveDirection = new THREE.Vector3(), 156 | angle; 157 | 158 | return function rotateCamera() { 159 | 160 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 161 | angle = moveDirection.length(); 162 | 163 | if ( angle ) { 164 | 165 | _eye.copy( _this.object.position ).sub( _this.target ); 166 | 167 | eyeDirection.copy( _eye ).normalize(); 168 | objectUpDirection.copy( _this.object.up ).normalize(); 169 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 170 | 171 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 172 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 173 | 174 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 175 | 176 | axis.crossVectors( moveDirection, _eye ).normalize(); 177 | 178 | angle *= _this.rotateSpeed; 179 | quaternion.setFromAxisAngle( axis, angle ); 180 | 181 | _eye.applyQuaternion( quaternion ); 182 | _this.object.up.applyQuaternion( quaternion ); 183 | 184 | _lastAxis.copy( axis ); 185 | _lastAngle = angle; 186 | 187 | } else if ( ! _this.staticMoving && _lastAngle ) { 188 | 189 | _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); 190 | _eye.copy( _this.object.position ).sub( _this.target ); 191 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 192 | _eye.applyQuaternion( quaternion ); 193 | _this.object.up.applyQuaternion( quaternion ); 194 | 195 | } 196 | 197 | _movePrev.copy( _moveCurr ); 198 | 199 | }; 200 | 201 | }() ); 202 | 203 | 204 | this.zoomCamera = function () { 205 | 206 | var factor; 207 | 208 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 209 | 210 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 211 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 212 | _eye.multiplyScalar( factor ); 213 | 214 | } else { 215 | 216 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 217 | 218 | if ( factor !== 1.0 && factor > 0.0 ) { 219 | 220 | _eye.multiplyScalar( factor ); 221 | 222 | } 223 | 224 | if ( _this.staticMoving ) { 225 | 226 | _zoomStart.copy( _zoomEnd ); 227 | 228 | } else { 229 | 230 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 231 | 232 | } 233 | 234 | } 235 | 236 | }; 237 | 238 | this.panCamera = ( function() { 239 | 240 | var mouseChange = new THREE.Vector2(), 241 | objectUp = new THREE.Vector3(), 242 | pan = new THREE.Vector3(); 243 | 244 | return function panCamera() { 245 | 246 | mouseChange.copy( _panEnd ).sub( _panStart ); 247 | 248 | if ( mouseChange.lengthSq() ) { 249 | 250 | mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 251 | 252 | pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 253 | pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 254 | 255 | _this.object.position.add( pan ); 256 | _this.target.add( pan ); 257 | 258 | if ( _this.staticMoving ) { 259 | 260 | _panStart.copy( _panEnd ); 261 | 262 | } else { 263 | 264 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 265 | 266 | } 267 | 268 | } 269 | 270 | }; 271 | 272 | }() ); 273 | 274 | this.checkDistances = function () { 275 | 276 | if ( ! _this.noZoom || ! _this.noPan ) { 277 | 278 | if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 279 | 280 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 281 | _zoomStart.copy( _zoomEnd ); 282 | 283 | } 284 | 285 | if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 286 | 287 | _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 288 | _zoomStart.copy( _zoomEnd ); 289 | 290 | } 291 | 292 | } 293 | 294 | }; 295 | 296 | this.update = function () { 297 | 298 | _eye.subVectors( _this.object.position, _this.target ); 299 | 300 | if ( ! _this.noRotate ) { 301 | 302 | _this.rotateCamera(); 303 | 304 | } 305 | 306 | if ( ! _this.noZoom ) { 307 | 308 | _this.zoomCamera(); 309 | 310 | } 311 | 312 | if ( ! _this.noPan ) { 313 | 314 | _this.panCamera(); 315 | 316 | } 317 | 318 | _this.object.position.addVectors( _this.target, _eye ); 319 | 320 | _this.checkDistances(); 321 | 322 | _this.object.lookAt( _this.target ); 323 | 324 | if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 325 | 326 | _this.dispatchEvent( changeEvent ); 327 | 328 | lastPosition.copy( _this.object.position ); 329 | 330 | } 331 | 332 | }; 333 | 334 | this.reset = function () { 335 | 336 | _state = STATE.NONE; 337 | _prevState = STATE.NONE; 338 | 339 | _this.target.copy( _this.target0 ); 340 | _this.object.position.copy( _this.position0 ); 341 | _this.object.up.copy( _this.up0 ); 342 | 343 | _eye.subVectors( _this.object.position, _this.target ); 344 | 345 | _this.object.lookAt( _this.target ); 346 | 347 | _this.dispatchEvent( changeEvent ); 348 | 349 | lastPosition.copy( _this.object.position ); 350 | 351 | }; 352 | 353 | // listeners 354 | 355 | function keydown( event ) { 356 | 357 | if ( _this.enabled === false ) return; 358 | 359 | window.removeEventListener( 'keydown', keydown ); 360 | 361 | _prevState = _state; 362 | 363 | if ( _state !== STATE.NONE ) { 364 | 365 | return; 366 | 367 | } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) { 368 | 369 | _state = STATE.ROTATE; 370 | 371 | } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) { 372 | 373 | _state = STATE.ZOOM; 374 | 375 | } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) { 376 | 377 | _state = STATE.PAN; 378 | 379 | } 380 | 381 | } 382 | 383 | function keyup( event ) { 384 | 385 | if ( _this.enabled === false ) return; 386 | 387 | _state = _prevState; 388 | 389 | window.addEventListener( 'keydown', keydown, false ); 390 | 391 | } 392 | 393 | function mousedown( event ) { 394 | 395 | if ( _this.enabled === false ) return; 396 | 397 | event.preventDefault(); 398 | event.stopPropagation(); 399 | 400 | if ( _state === STATE.NONE ) { 401 | 402 | _state = event.button; 403 | 404 | } 405 | 406 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 407 | 408 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 409 | _movePrev.copy( _moveCurr ); 410 | 411 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 412 | 413 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 414 | _zoomEnd.copy( _zoomStart ); 415 | 416 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 417 | 418 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 419 | _panEnd.copy( _panStart ); 420 | 421 | } 422 | 423 | document.addEventListener( 'mousemove', mousemove, false ); 424 | document.addEventListener( 'mouseup', mouseup, false ); 425 | 426 | _this.dispatchEvent( startEvent ); 427 | 428 | } 429 | 430 | function mousemove( event ) { 431 | 432 | if ( _this.enabled === false ) return; 433 | 434 | event.preventDefault(); 435 | event.stopPropagation(); 436 | 437 | if ( _state === STATE.ROTATE && ! _this.noRotate ) { 438 | 439 | _movePrev.copy( _moveCurr ); 440 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 441 | 442 | } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 443 | 444 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 445 | 446 | } else if ( _state === STATE.PAN && ! _this.noPan ) { 447 | 448 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 449 | 450 | } 451 | 452 | } 453 | 454 | function mouseup( event ) { 455 | 456 | if ( _this.enabled === false ) return; 457 | 458 | event.preventDefault(); 459 | event.stopPropagation(); 460 | 461 | _state = STATE.NONE; 462 | 463 | document.removeEventListener( 'mousemove', mousemove ); 464 | document.removeEventListener( 'mouseup', mouseup ); 465 | _this.dispatchEvent( endEvent ); 466 | 467 | } 468 | 469 | function mousewheel( event ) { 470 | 471 | if ( _this.enabled === false ) return; 472 | 473 | if ( _this.noZoom === true ) return; 474 | 475 | event.preventDefault(); 476 | event.stopPropagation(); 477 | 478 | switch ( event.deltaMode ) { 479 | 480 | case 2: 481 | // Zoom in pages 482 | _zoomStart.y -= event.deltaY * 0.025; 483 | break; 484 | 485 | case 1: 486 | // Zoom in lines 487 | _zoomStart.y -= event.deltaY * 0.01; 488 | break; 489 | 490 | default: 491 | // undefined, 0, assume pixels 492 | _zoomStart.y -= event.deltaY * 0.00025; 493 | break; 494 | 495 | } 496 | 497 | _this.dispatchEvent( startEvent ); 498 | _this.dispatchEvent( endEvent ); 499 | 500 | } 501 | 502 | function touchstart( event ) { 503 | 504 | if ( _this.enabled === false ) return; 505 | 506 | switch ( event.touches.length ) { 507 | 508 | case 1: 509 | _state = STATE.TOUCH_ROTATE; 510 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 511 | _movePrev.copy( _moveCurr ); 512 | break; 513 | 514 | default: // 2 or more 515 | _state = STATE.TOUCH_ZOOM_PAN; 516 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 517 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 518 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 519 | 520 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 521 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 522 | _panStart.copy( getMouseOnScreen( x, y ) ); 523 | _panEnd.copy( _panStart ); 524 | break; 525 | 526 | } 527 | 528 | _this.dispatchEvent( startEvent ); 529 | 530 | } 531 | 532 | function touchmove( event ) { 533 | 534 | if ( _this.enabled === false ) return; 535 | 536 | event.preventDefault(); 537 | event.stopPropagation(); 538 | 539 | switch ( event.touches.length ) { 540 | 541 | case 1: 542 | _movePrev.copy( _moveCurr ); 543 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 544 | break; 545 | 546 | default: // 2 or more 547 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 548 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 549 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 550 | 551 | var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 552 | var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 553 | _panEnd.copy( getMouseOnScreen( x, y ) ); 554 | break; 555 | 556 | } 557 | 558 | } 559 | 560 | function touchend( event ) { 561 | 562 | if ( _this.enabled === false ) return; 563 | 564 | switch ( event.touches.length ) { 565 | 566 | case 0: 567 | _state = STATE.NONE; 568 | break; 569 | 570 | case 1: 571 | _state = STATE.TOUCH_ROTATE; 572 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 573 | _movePrev.copy( _moveCurr ); 574 | break; 575 | 576 | } 577 | 578 | _this.dispatchEvent( endEvent ); 579 | 580 | } 581 | 582 | function contextmenu( event ) { 583 | 584 | if ( _this.enabled === false ) return; 585 | 586 | event.preventDefault(); 587 | 588 | } 589 | 590 | this.dispose = function() { 591 | 592 | this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); 593 | this.domElement.removeEventListener( 'mousedown', mousedown, false ); 594 | this.domElement.removeEventListener( 'wheel', mousewheel, false ); 595 | 596 | this.domElement.removeEventListener( 'touchstart', touchstart, false ); 597 | this.domElement.removeEventListener( 'touchend', touchend, false ); 598 | this.domElement.removeEventListener( 'touchmove', touchmove, false ); 599 | 600 | document.removeEventListener( 'mousemove', mousemove, false ); 601 | document.removeEventListener( 'mouseup', mouseup, false ); 602 | 603 | window.removeEventListener( 'keydown', keydown, false ); 604 | window.removeEventListener( 'keyup', keyup, false ); 605 | 606 | }; 607 | 608 | this.domElement.addEventListener( 'contextmenu', contextmenu, false ); 609 | this.domElement.addEventListener( 'mousedown', mousedown, false ); 610 | this.domElement.addEventListener( 'wheel', mousewheel, false ); 611 | 612 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 613 | this.domElement.addEventListener( 'touchend', touchend, false ); 614 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 615 | 616 | window.addEventListener( 'keydown', keydown, false ); 617 | window.addEventListener( 'keyup', keyup, false ); 618 | 619 | this.handleResize(); 620 | 621 | // force an update at start 622 | this.update(); 623 | 624 | }; 625 | 626 | THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 627 | THREE.TrackballControls.prototype.constructor = THREE.TrackballControls; 628 | -------------------------------------------------------------------------------- /octoprint_DuCalibrator/static/js/DuCalViewModel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * View model for Delta Micro Calibrator 3 | * 4 | * Author: Fabio Santos 5 | * License: AGPLv3 6 | */ 7 | class DuCalibratorViewModel { 8 | constructor(parameters) { 9 | 10 | // dependencies 11 | this.settingsViewModel = parameters[0]; 12 | this.printerProfilesViewModel = parameters[1]; 13 | 14 | // privates 15 | this.trialNumber = 0; 16 | this.plot = undefined; 17 | this.zScaleInfo = undefined; 18 | this.settings = undefined; 19 | 20 | // parameters 21 | this.probeRadius = ko.observable(this.printerProfilesViewModel.currentProfileData().volume.width() / 2); 22 | this.probePointCount = ko.observable(50); 23 | 24 | // UI control 25 | this.isGeometryKnown = () => this.machine().Geometry() != undefined; 26 | this.isReadyForCommands = ()=> this.machine().IsReady() && !this.machine().IsBusy(); 27 | 28 | this.isFetchingGeometry = ko.observable(false); 29 | this.isProbing = ko.observable(false); 30 | this.isCalibrating = ko.observable(false); 31 | this.probingProgressString = ko.observable("0"); 32 | 33 | this.showProbedPoints = ko.observable(true); 34 | this.showCalibratedPoints = ko.observable(true); 35 | this.showAsCorrections = ko.observable("true"); 36 | 37 | this.isReadyToCalibrate = ko.observable(false); 38 | 39 | this.plotDivElement = $("#surfacePlotDiv")[0]; 40 | this.GeometryControl = new CollapseControl(); 41 | this.PlotControl = new CollapseControl(); 42 | this.CalibrationControl = new CollapseControl(); 43 | this.HistoryControl = new CollapseControl(); 44 | 45 | // Observable data 46 | this.currentGeometry = ()=>this.machine()?.Geometry(); 47 | this.newGeometry = ko.observable(new DeltaGeometry()); 48 | this.ProbedData = new ProbingData().Observable; 49 | this.CalibratedData = ko.observable(undefined); 50 | this.probedGeometries = ko.observableArray([]); 51 | 52 | this.calibrate = 53 | { 54 | StepsPerUnit: ko.observable(false), 55 | EndStopOffset: ko.observable(true), 56 | TowerOffset: ko.observable(true), 57 | RodLength: ko.observable(true), 58 | RodLenghtAdjust: ko.observable(false), 59 | DeltaRadius: ko.observable(true), 60 | DeltaRadiusAdjust: ko.observable(false), 61 | MaxHeight: ko.observable(true) 62 | }; 63 | 64 | this.calibrate.StepsPerUnit.subscribe(this.computeCorrections, this); 65 | this.calibrate.EndStopOffset.subscribe(this.computeCorrections, this); 66 | this.calibrate.TowerOffset.subscribe(this.computeCorrections, this); 67 | this.calibrate.RodLength.subscribe(this.computeCorrections, this); 68 | this.calibrate.RodLenghtAdjust.subscribe(this.computeCorrections, this); 69 | this.calibrate.DeltaRadius.subscribe(this.computeCorrections, this); 70 | this.calibrate.DeltaRadiusAdjust.subscribe(this.computeCorrections, this); 71 | this.calibrate.MaxHeight.subscribe(this.computeCorrections, this); 72 | 73 | this.machine = new ko.observable(undefined); 74 | } 75 | 76 | //hooks 77 | onSettingsBeforeSave() { 78 | this.ReloadSettings() 79 | } 80 | 81 | onBeforeBinding() { 82 | this.ReloadSettings() 83 | } 84 | 85 | fromCurrentData(data) { 86 | this.fromHistoryData(data); 87 | this.machine()?.ParseData(data); 88 | } 89 | 90 | fromHistoryData(data) { 91 | this.latestData = data; 92 | } 93 | 94 | // events 95 | ReloadSettings() 96 | { 97 | this.resetCalibrationData(); 98 | this.resetProbeData(); 99 | this.probedGeometries([]); 100 | this.PlotControl.Hide(); 101 | 102 | this.settings = this.settingsViewModel.settings.plugins.DuCalibrator; 103 | switch(this.settings.Firmware()) 104 | { 105 | case "Marlin": 106 | this.machine(new MarlinMachine(this.settings)); 107 | break; 108 | case "Smoothie": 109 | this.machine(new SmoothieMachine(this.settings)); 110 | break; 111 | case "Simulated": 112 | { 113 | const testGeo = new DeltaGeometry(330, 165, 300, [0,0,0], [0, 0, 0], [400,400,400]); 114 | const initialGeo = new DeltaGeometry( 115 | testGeo.DiagonalRod + (1 - Math.random() * 2), 116 | testGeo.Radius + (1 - Math.random() * 2), 117 | testGeo.Height, 118 | [testGeo.EndStopOffset[0] + Math.random(), testGeo.EndStopOffset[1] + Math.random(), testGeo.EndStopOffset[2] + Math.random()], 119 | [testGeo.TowerOffset[0] + (1 - Math.random() * 2), testGeo.TowerOffset[1] + (1 - Math.random() * 2), testGeo.TowerOffset[2] + (1 - Math.random() * 2)], 120 | testGeo.StepsPerUnit.slice(), 121 | testGeo.RadiusAdjust.slice(), 122 | [testGeo.DiagonalRodAdjust[0] + (1 - Math.random() * 2), testGeo.DiagonalRodAdjust[1] + (1 - Math.random() * 2), testGeo.DiagonalRodAdjust[2] + (1 - Math.random() * 2)]); 123 | initialGeo.Adjust([false],[0]); 124 | 125 | this.machine(new TestMachine(this.settings, testGeo, initialGeo)); 126 | break; 127 | } 128 | } 129 | 130 | // Some versions of octoprint call onBeforeBind before fromHistoryData 131 | if(this.latestData) 132 | this.machine().ParseData(this.latestData); 133 | 134 | this.GeometryControl.Hide(); 135 | } 136 | 137 | // helpers 138 | resetProbeData() { 139 | this.isReadyToCalibrate(false); 140 | new ProbingData(this.ProbedData); 141 | 142 | if (this.plot) { 143 | this.plot.probedParticles.geometry.dispose(); 144 | this.plot.correctedParticles.geometry.dispose(); 145 | this.plot = null; 146 | } 147 | 148 | if (this.plotDivElement.firstChild) 149 | this.plotDivElement.removeChild(this.plotDivElement.firstChild); 150 | } 151 | 152 | resetCalibrationData() { 153 | this.CalibratedData(undefined); 154 | this.CalibrationControl.Hide(); 155 | } 156 | 157 | computeCorrections() { 158 | var factors = [ 159 | this.calibrate.EndStopOffset(), 160 | this.calibrate.EndStopOffset(), 161 | this.calibrate.EndStopOffset() && !this.calibrate.MaxHeight(), 162 | this.calibrate.DeltaRadius(), 163 | this.calibrate.TowerOffset(), 164 | this.calibrate.TowerOffset(), 165 | this.calibrate.RodLength(), 166 | this.calibrate.StepsPerUnit(), 167 | this.calibrate.StepsPerUnit(), 168 | this.calibrate.StepsPerUnit(), 169 | this.calibrate.DeltaRadiusAdjust(), 170 | this.calibrate.DeltaRadiusAdjust(), 171 | this.calibrate.DeltaRadiusAdjust() && !this.calibrate.DeltaRadius(), 172 | this.calibrate.RodLenghtAdjust(), 173 | this.calibrate.RodLenghtAdjust(), 174 | this.calibrate.RodLenghtAdjust() && !this.calibrate.RodLength(), 175 | this.calibrate.MaxHeight()]; 176 | 177 | var result = DeltaGeometry.Calibrate(this.currentGeometry().Clone(), this.ProbedData(), factors); 178 | 179 | for (var i = 0; i < result.Residuals.length; ++i) 180 | { 181 | this.plot.correctedParticles.geometry.attributes.position.setXYZ(i, result.Residuals[i].X, result.Residuals[i].Y, result.Residuals[i].Z * this.zScaleInfo.zScale); 182 | } 183 | 184 | this.plot.correctedParticles.geometry.setDrawRange(0, result.Residuals.length); 185 | this.plot.correctedParticles.geometry.attributes.position.needsUpdate = true; 186 | this.plot.probedParticles.material.opacity = 0.5; 187 | this.plot.probedParticles.material.size = 3; 188 | this.plot.probedParticles.material.transparent = true; 189 | this.plot.probedParticles.material.needsUpdate = true; 190 | 191 | this.CalibratedData(result); 192 | this.newGeometry(result.Geometry); 193 | 194 | console.log(result); 195 | } 196 | 197 | // 3d visualization 198 | preparePlot(surfacePlotDiv, bedRadius, probeCount) 199 | { 200 | var scene = new THREE.Scene(); 201 | scene.background = new THREE.Color(0xffffff); 202 | //scene.fog = new THREE.FogExp2(0xffffff, 0.002); 203 | var renderer = new THREE.WebGLRenderer(); 204 | 205 | renderer.setSize(surfacePlotDiv.clientWidth, surfacePlotDiv.clientHeight); 206 | if (surfacePlotDiv.firstChild) surfacePlotDiv.removeChild(surfacePlotDiv.firstChild); 207 | surfacePlotDiv.appendChild(renderer.domElement); 208 | 209 | 210 | // Axes arrows 211 | scene.add(new THREE.ArrowHelper(new THREE.Vector3(bedRadius, 0, 0), new THREE.Vector3(-bedRadius, -bedRadius, 0), bedRadius / 2, 0xff0000)); 212 | scene.add(new THREE.ArrowHelper(new THREE.Vector3(0, bedRadius, 0), new THREE.Vector3(-bedRadius, -bedRadius, 0), bedRadius / 2, 0x00ff00)); 213 | scene.add(new THREE.ArrowHelper(new THREE.Vector3(0, 0, bedRadius), new THREE.Vector3(-bedRadius, -bedRadius, 0), bedRadius / 2, 0x0000ff)); 214 | 215 | var geometry = new THREE.BufferGeometry(); 216 | var vertices = new Float32Array(probeCount * 3).fill(0); // x,y,z 217 | geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); 218 | var probedParticles = new THREE.Points(geometry, new THREE.PointsMaterial({ color: 0xff0000, size: 5, sizeAttenuation: true, transparent: true, opacity: 1 })); 219 | probedParticles.geometry.setDrawRange(0, 0); 220 | scene.add(probedParticles); 221 | 222 | var correctedGeometry = new THREE.BufferGeometry(); 223 | var correctedVertices = new Float32Array(probeCount * 3).fill(0); // x,y,z 224 | correctedGeometry.setAttribute('position', new THREE.BufferAttribute(correctedVertices, 3)); 225 | var correctedParticles = new THREE.Points(correctedGeometry, new THREE.PointsMaterial({ color: 0x00bb00, size: 5, sizeAttenuation: true })); 226 | correctedParticles.geometry.setDrawRange(0, 0); 227 | scene.add(correctedParticles); 228 | 229 | // add the bed plate for reference 230 | scene.add( 231 | new THREE.Mesh( 232 | (new THREE.CylinderBufferGeometry(bedRadius, bedRadius, 0.001, 32)) 233 | .rotateX(Math.PI / 2), 234 | new THREE.MeshBasicMaterial({ color: 0x8080FF, opacity: 0.3, transparent: true }))); 235 | 236 | // camera 237 | var camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000); 238 | camera.position.set(0, -bedRadius * 4, bedRadius); 239 | var controls = new THREE.TrackballControls(camera, renderer.domElement); 240 | controls.minDistance = 10; 241 | controls.maxDistance = bedRadius * 5; 242 | 243 | this.showProbedPoints(true); 244 | this.showCalibratedPoints(true); 245 | 246 | var animate = function () { 247 | requestAnimationFrame(animate); 248 | 249 | controls.update(); 250 | renderer.render(scene, camera); 251 | }; 252 | 253 | animate(); 254 | 255 | return {probedParticles: probedParticles, correctedParticles: correctedParticles}; 256 | } 257 | 258 | logProbePoint(x, y, z) 259 | { 260 | this.ProbedData.peek().AddPoint(new ProbePoint([x, y, 0], [x, y, z])); 261 | var newScale = this.adjustZScale(this.zScaleInfo, z); 262 | this.plot.probedParticles.geometry.scale(1, 1, newScale); 263 | 264 | this.plot.probedParticles.geometry.attributes.position.setXYZ(this.ProbedData.peek().DataPoints.length - 1, x, y, z * this.zScaleInfo.zScale); 265 | this.plot.probedParticles.geometry.setDrawRange(0, this.ProbedData.peek().DataPoints.length); 266 | this.plot.probedParticles.geometry.attributes.position.needsUpdate = true; 267 | } 268 | 269 | adjustZScale(zScaleInfo, z) 270 | { 271 | if ((zScaleInfo.maxZ === undefined) || (z > zScaleInfo.maxZ)) 272 | zScaleInfo.maxZ = Math.max(z, 1e-3); 273 | if ((zScaleInfo.minZ === undefined) || (z < zScaleInfo.minZ)) 274 | zScaleInfo.minZ = Math.min(z, -1e-3); 275 | 276 | if (zScaleInfo.maxZ === zScaleInfo.minZ) 277 | return 1; 278 | 279 | var oldScale = zScaleInfo.zScale; 280 | var newScale = zScaleInfo.normalizeTo / (zScaleInfo.maxZ - zScaleInfo.minZ); 281 | zScaleInfo.zScale = newScale; 282 | 283 | return newScale / oldScale; 284 | } 285 | 286 | // ui commands 287 | updateVisiblePoints() 288 | { 289 | this.plot.probedParticles.visible = this.showProbedPoints(); 290 | this.plot.correctedParticles.visible = this.showCalibratedPoints(); 291 | 292 | this.plot.probedParticles.material.size = this.showProbedPoints() && this.showCalibratedPoints() ? 3 : 5; 293 | this.plot.probedParticles.material.needsUpdate = true; 294 | 295 | return true; 296 | } 297 | 298 | async applyCalibration() { 299 | await this.ConfigureGeometry(this.newGeometry(), false); 300 | } 301 | 302 | cancelProbing() 303 | { 304 | this.cancelProbingRequested = true; 305 | } 306 | 307 | async ConfigureGeometry(geometry, save) 308 | { 309 | this.isCalibrating(true); 310 | try 311 | { 312 | await this.machine().SetGeometry(geometry, save); 313 | this.GeometryControl.Show(); 314 | } 315 | catch(err) 316 | { 317 | console.log(err); 318 | this.ReloadSettings(); 319 | } 320 | finally 321 | { 322 | this.resetProbeData(); 323 | this.resetCalibrationData(); 324 | this.PlotControl.Hide(); 325 | this.isCalibrating(false); 326 | } 327 | } 328 | 329 | async probeBed() 330 | { 331 | this.cancelProbingRequested = false; 332 | this.isProbing(true); 333 | this.resetProbeData(); 334 | 335 | // fetching the actual printer radius so the plot look to scale 336 | var radius = this.printerProfilesViewModel.currentProfileData().volume.width() / 2; 337 | 338 | this.zScaleInfo = { 339 | minZ: 0, 340 | maxZ: 0, 341 | zScale: 1, 342 | normalizeTo: radius / 3 343 | }; 344 | 345 | this.PlotControl.Show(); 346 | this.plot = this.preparePlot( 347 | this.plotDivElement, 348 | radius, 349 | this.probePointCount()); 350 | 351 | var points = DuCalUtils.GetSpiralPoints(this.probePointCount(), this.probeRadius()); 352 | for(const point of points) 353 | { 354 | if(this.cancelProbingRequested) 355 | { 356 | this.resetProbeData(); 357 | this.resetCalibrationData(); 358 | this.PlotControl.Hide(); 359 | this.isProbing(false); 360 | this.cancelProbingRequested=false; 361 | return; 362 | } 363 | 364 | try 365 | { 366 | const probe = await this.machine().ProbeBed(point[0],point[1], point === points[points.length-1]); 367 | if(probe) 368 | this.logProbePoint(probe[0], probe[1], probe[2]); 369 | 370 | this.probingProgressString((this.ProbedData.peek().DataPoints.length/points.length*100).toFixed(0)); 371 | } 372 | catch(err) 373 | { 374 | console.log(err); 375 | this.PlotControl.Hide(); 376 | this.isProbing(false); 377 | this.cancelProbingRequested=false; 378 | this.ReloadSettings(); 379 | return; 380 | } 381 | } 382 | 383 | this.probedGeometries.unshift({ 384 | Name: "Trial #" + this.trialNumber++, 385 | Timestamp: new Date().toLocaleString(), 386 | RMS: this.ProbedData.peek().RMS.toFixed(3), 387 | Geometry: this.currentGeometry().Clone() 388 | }); 389 | 390 | this.isReadyToCalibrate(true); 391 | this.computeCorrections(); 392 | this.GeometryControl.Hide(); 393 | this.CalibrationControl.Show(); 394 | this.isProbing(false); 395 | } 396 | 397 | async fetchGeometry() { 398 | this.isFetchingGeometry(true); 399 | this.resetProbeData(); 400 | try 401 | { 402 | var newGeometry = await this.machine().GetGeometry(); 403 | this.GeometryControl.Show(); 404 | } 405 | catch(err) 406 | { 407 | console.log(err); 408 | this.ReloadSettings(); 409 | } 410 | finally 411 | { 412 | this.resetCalibrationData(); 413 | this.PlotControl.Hide(); 414 | this.isFetchingGeometry(false); 415 | } 416 | } 417 | 418 | async SaveGeometry(data) { 419 | await this.ConfigureGeometry(data.Geometry, true); 420 | } 421 | 422 | } 423 | 424 | $(function () { 425 | /* view model class, parameters for constructor, container to bind to 426 | * Please see http://docs.octoprint.org/en/master/plugins/viewmodels.html#registering-custom-viewmodels for more details 427 | * and a full list of the available options. 428 | */ 429 | OCTOPRINT_VIEWMODELS.push({ 430 | construct: DuCalibratorViewModel, 431 | // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ... 432 | dependencies: [ /* "loginStateViewModel", */ "settingsViewModel", "printerProfilesViewModel"], 433 | // Elements to bind to, e.g. #settings_plugin_DuCalibrator, #tab_plugin_DuCalibrator, ... 434 | elements: ["#tab_plugin_DuCalibrator", "#settings_plugin_DuCalibrator"] 435 | }); 436 | }); 437 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . --------------------------------------------------------------------------------