";
233 | toolTip += T("Time: {0}", formatTime(timeUsed)) + " ";
234 | toolTip += T("Filament usage: {0} mm", filamentUsed.toFixed(1));
235 |
236 | // Show it
237 | $("#layer_tooltip").html(toolTip).css({top: item.pageY + 5, left: item.pageX + 5}).fadeIn(200);
238 | } else {
239 | $("#layer_tooltip").hide();
240 | }
241 | });
242 | }
243 |
244 |
245 | /* Common functions */
246 |
247 | function resizeCharts() {
248 | var contentHeight = $("#table_tools").height();
249 | if (!$("#div_heaters").hasClass("hidden")) {
250 | contentHeight = $("#table_heaters").height();
251 | } else if (!$("#div_extra").hasClass("hidden")) {
252 | contentHeight = $("#table_extra").height();
253 | }
254 |
255 | var statusHeight = 0;
256 | $("#div_status table").each(function() {
257 | statusHeight += $(this).outerHeight();
258 | });
259 |
260 | var max = (contentHeight > statusHeight) ? contentHeight : statusHeight;
261 | var padding = $("#chart_temp").parent().outerHeight() - $("#chart_temp").height();
262 | max -= padding;
263 |
264 | if (max > 0) {
265 | $("#chart_temp").css("height", max);
266 | }
267 |
268 | if (refreshTempChart) {
269 | drawTemperatureChart();
270 | }
271 | if (refreshPrintChart) {
272 | drawPrintChart();
273 | }
274 | }
275 |
276 | $(".panel-chart").resize(function() {
277 | resizeCharts();
278 | });
279 |
280 | function resetChartData() {
281 | // Initialize visibility states of the extra temperature sensors
282 | extraSensorVisibility = getLocalSetting("extraSensorVisibility", null);
283 | if (extraSensorVisibility == null || extraSensorVisibility.length < maxTempSensors) {
284 | extraSensorVisibility = [];
285 | for(var i = 0; i < maxTempSensors; i++) {
286 | // Don't show any extra temperatures in the chart by default
287 | extraSensorVisibility.push(false);
288 | }
289 | }
290 |
291 | // Reset data of the temperature chart
292 | recordedTemperatures = [];
293 | for(var i = 0; i < maxHeaters; i++) {
294 | recordedTemperatures.push([]);
295 | }
296 |
297 | for(var i = 0; i < maxTempSensors; i++) {
298 | recordedTemperatures.push({
299 | dashes: { show: extraSensorVisibility[i] },
300 | lines: { show: false },
301 | data: []
302 | });
303 |
304 | $("#table_extra tr input[type='checkbox']").eq(i).prop("checked", extraSensorVisibility[i]);
305 | }
306 |
307 | // Reset data of the layer chart
308 | layerData = [];
309 | maxLayerTime = 0;
310 | }
311 |
--------------------------------------------------------------------------------
/core/css/bootstrap-slider.css:
--------------------------------------------------------------------------------
1 | /*! =======================================================
2 | VERSION 6.1.4
3 | ========================================================= */
4 | /*! =========================================================
5 | * bootstrap-slider.js
6 | *
7 | * Maintainers:
8 | * Kyle Kemp
9 | * - Twitter: @seiyria
10 | * - Github: seiyria
11 | * Rohit Kalkur
12 | * - Twitter: @Rovolutionary
13 | * - Github: rovolution
14 | *
15 | * =========================================================
16 | *
17 | * Licensed under the Apache License, Version 2.0 (the "License");
18 | * you may not use this file except in compliance with the License.
19 | * You may obtain a copy of the License at
20 | *
21 | * http://www.apache.org/licenses/LICENSE-2.0
22 | *
23 | * Unless required by applicable law or agreed to in writing, software
24 | * distributed under the License is distributed on an "AS IS" BASIS,
25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26 | * See the License for the specific language governing permissions and
27 | * limitations under the License.
28 | * ========================================================= */
29 | .slider {
30 | display: inline-block;
31 | vertical-align: middle;
32 | position: relative;
33 | }
34 | .slider.slider-horizontal {
35 | width: 210px;
36 | height: 20px;
37 | }
38 | .slider.slider-horizontal .slider-track {
39 | height: 10px;
40 | width: 100%;
41 | margin-top: -5px;
42 | top: 50%;
43 | left: 0;
44 | }
45 | .slider.slider-horizontal .slider-selection,
46 | .slider.slider-horizontal .slider-track-low,
47 | .slider.slider-horizontal .slider-track-high {
48 | height: 100%;
49 | top: 0;
50 | bottom: 0;
51 | }
52 | .slider.slider-horizontal .slider-tick,
53 | .slider.slider-horizontal .slider-handle {
54 | margin-left: -10px;
55 | margin-top: -5px;
56 | }
57 | .slider.slider-horizontal .slider-tick.triangle,
58 | .slider.slider-horizontal .slider-handle.triangle {
59 | border-width: 0 10px 10px 10px;
60 | width: 0;
61 | height: 0;
62 | border-bottom-color: #0480be;
63 | margin-top: 0;
64 | }
65 | .slider.slider-horizontal .slider-tick-label-container {
66 | white-space: nowrap;
67 | margin-top: 20px;
68 | }
69 | .slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
70 | padding-top: 4px;
71 | display: inline-block;
72 | text-align: center;
73 | }
74 | .slider.slider-vertical {
75 | height: 210px;
76 | width: 20px;
77 | }
78 | .slider.slider-vertical .slider-track {
79 | width: 10px;
80 | height: 100%;
81 | margin-left: -5px;
82 | left: 50%;
83 | top: 0;
84 | }
85 | .slider.slider-vertical .slider-selection {
86 | width: 100%;
87 | left: 0;
88 | top: 0;
89 | bottom: 0;
90 | }
91 | .slider.slider-vertical .slider-track-low,
92 | .slider.slider-vertical .slider-track-high {
93 | width: 100%;
94 | left: 0;
95 | right: 0;
96 | }
97 | .slider.slider-vertical .slider-tick,
98 | .slider.slider-vertical .slider-handle {
99 | margin-left: -5px;
100 | margin-top: -10px;
101 | }
102 | .slider.slider-vertical .slider-tick.triangle,
103 | .slider.slider-vertical .slider-handle.triangle {
104 | border-width: 10px 0 10px 10px;
105 | width: 1px;
106 | height: 1px;
107 | border-left-color: #0480be;
108 | margin-left: 0;
109 | }
110 | .slider.slider-vertical .slider-tick-label-container {
111 | white-space: nowrap;
112 | }
113 | .slider.slider-vertical .slider-tick-label-container .slider-tick-label {
114 | padding-left: 4px;
115 | }
116 | .slider.slider-disabled .slider-handle {
117 | background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
118 | background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
119 | background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
120 | background-repeat: repeat-x;
121 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
122 | }
123 | .slider.slider-disabled .slider-track {
124 | background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
125 | background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
126 | background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
127 | background-repeat: repeat-x;
128 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
129 | cursor: not-allowed;
130 | }
131 | .slider input {
132 | display: none;
133 | }
134 | .slider .tooltip.top {
135 | margin-top: -36px;
136 | }
137 | .slider .tooltip-inner {
138 | white-space: nowrap;
139 | max-width: none;
140 | }
141 | .slider .hide {
142 | display: none;
143 | }
144 | .slider-track {
145 | position: absolute;
146 | cursor: pointer;
147 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
148 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
149 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
150 | background-repeat: repeat-x;
151 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
152 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
153 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
154 | border-radius: 4px;
155 | }
156 | .slider-selection {
157 | position: absolute;
158 | background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
159 | background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
160 | background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
161 | background-repeat: repeat-x;
162 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
163 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
164 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
165 | -webkit-box-sizing: border-box;
166 | -moz-box-sizing: border-box;
167 | box-sizing: border-box;
168 | border-radius: 4px;
169 | }
170 | .slider-selection.tick-slider-selection {
171 | background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
172 | background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
173 | background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
174 | background-repeat: repeat-x;
175 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
176 | }
177 | .slider-track-low,
178 | .slider-track-high {
179 | position: absolute;
180 | background: transparent;
181 | -webkit-box-sizing: border-box;
182 | -moz-box-sizing: border-box;
183 | box-sizing: border-box;
184 | border-radius: 4px;
185 | }
186 | .slider-handle {
187 | position: absolute;
188 | width: 20px;
189 | height: 20px;
190 | background-color: #337ab7;
191 | background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
192 | background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
193 | background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
194 | background-repeat: repeat-x;
195 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
196 | filter: none;
197 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
198 | box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
199 | border: 0px solid transparent;
200 | }
201 | .slider-handle.round {
202 | border-radius: 50%;
203 | }
204 | .slider-handle.triangle {
205 | background: transparent none;
206 | }
207 | .slider-handle.custom {
208 | background: transparent none;
209 | }
210 | .slider-handle.custom::before {
211 | line-height: 20px;
212 | font-size: 20px;
213 | content: '\2605';
214 | color: #726204;
215 | }
216 | .slider-tick {
217 | position: absolute;
218 | width: 20px;
219 | height: 20px;
220 | background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
221 | background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
222 | background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
223 | background-repeat: repeat-x;
224 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
225 | -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
226 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
227 | -webkit-box-sizing: border-box;
228 | -moz-box-sizing: border-box;
229 | box-sizing: border-box;
230 | filter: none;
231 | opacity: 0.8;
232 | border: 0px solid transparent;
233 | }
234 | .slider-tick.round {
235 | border-radius: 50%;
236 | }
237 | .slider-tick.triangle {
238 | background: transparent none;
239 | }
240 | .slider-tick.custom {
241 | background: transparent none;
242 | }
243 | .slider-tick.custom::before {
244 | line-height: 20px;
245 | font-size: 20px;
246 | content: '\2605';
247 | color: #726204;
248 | }
249 | .slider-tick.in-selection {
250 | background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
251 | background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
252 | background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
253 | background-repeat: repeat-x;
254 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
255 | opacity: 1;
256 | }
257 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Duet Web Control
2 |
3 | Duet Web Control is a fully-responsive HTML5-based web interface for RepRapFirmware which utilizes the Bootstrap framework, JQuery and a few other libraries to allow easy control of Duet-based 3D printer electronics.
4 |
5 | It is designed to communicate with RepRapFirmware using HTTP GET requests and to upload single files using an HTTP POST request. One goal of the core application is to keep things compact, so a good loading speed can be achieved even on slow networks. Another one is to communicate to the firmware using only AJAX calls, which either return JSON objects, plain texts or binary blobs.
6 |
7 | Duet Web Control is free software; it is licensed under the terms of the GNU Public License v2.
8 |
9 | ## Supported electronics
10 |
11 | At this time the following platforms are supported:
12 |
13 | * Duet 0.6
14 | * Duet 0.8.5
15 | * Duet WiFi
16 | * Duet Ethernet
17 |
18 | ## Communication to the firmware
19 |
20 | Since RepRapFirmware can only process one HTTP request at a time (excluding rr_fileinfo and rr_upload on certain platforms), Duet Web Control should attempt to avoid parallel requests. In general, the communication between the web interface and RepRapFirmware looks like this:
21 |
22 | - Establish a connection (via rr_connect)
23 | - Send an extended status request (rr_status?type=2) and start status update loop
24 | - Load macros (rr_filelist?dir=/macros)
25 | - (User switches to "G-Code Files" tab)
26 | - Stop automatic status updates
27 | - Load G-code filelist (rr_filelist?dir=/gcodes)
28 | - File info about each file is requested (unless cached values are available)
29 | - Start automatic status requests again
30 | - (User does something else)
31 | - DWC disconnects (via rr_disconnect)
32 |
33 | Note the interrupt of live updates while multiple long requests are processed. DWC implements two particular functions (stopUpdates and startUpdates) which can - and should - be used to stop status requests while long-running HTTP requests are being executed. The update loop is stopped when file uploads are started, too, however it is not required to interrupt the update loop while short requests (e.g. rr_gcode) are sent.
34 |
35 | Some requests may send or expect date and time values. These values are represented by the format "YYYY-MM-DDTHH:MM:SS" similar to the ISO-8601 format.
36 |
37 | ## List of HTTP requests
38 |
39 | All HTTP requests, except for rr_upload, are simple GET requests that return JSON objects, which makes it easy to deal with them using JavaScript code. Here the list of all currently used requests:
40 |
41 | #### rr_connect?password=XXX&time=YYY
42 | Create an initial connection between DWC and RRF.
43 | - On success, the firmware sends out a response like: {"err":0,"sessionTimeout":[time in ms],"boardType":"[board type]"} This way DWC can adjust the AJAX timeout value and set board-specific options. The "time" value should represent the client's date and time to set the on-board RTC if necessary.
44 | - If anything goes wrong, the firmware only responds with an {"err":[code]} object. If code is 1, then the specified password is wrong. If it is 2, then the firmware cannot allocate enough resources to accomodate another session.
45 |
46 | #### rr_disconnect
47 | Delete an existing HTTP session. This should be used to log off manually, however sessions are usually purged automatically if no communication takes place within the time specified in "sessionTimeout" above.
48 |
49 | #### rr_status?type=XXX
50 | Request a status response from the firmware which usually includes all the machine parameters that are expected to change from time to time. This makes it possible to display live values like XYZ position and heater temperatures. This type of request is usually sent to the firmware in rather short intervals (250ms by default). At this time there are three different supported status request types, which may be polled in different intervals:
51 |
52 | - Type 1: Regular status request. The response for this is usually rather compact and only includes values that are expected to change quickly. The following types 2 and 3 include those values under any circumstances to keep the web interface up-to-date.
53 | - Type 2: Extended status request. This type of request is polled right after a connection has been established. This response provides information about the tool mapping and values that can change.
54 | - Type 3: Print status request. Unlike type 2, this type of request is always polled when a file print is in progress. It provides print time estimations and other print-related information which can be shown on the print status page.
55 |
56 | #### rr_gcode?gcode=XXX
57 | Send a G-code to the firmware. Since RepRapFirmware is generally only controlled by G-codes, this provides an interface to transmit codes from the web interface. This request returns the amount of currently available buffer space for incoming G-codes, however DWC does not actively use this response yet.
58 |
59 | #### rr_upload?name=XXX&time=YYY
60 | Upload a file to path XXX with the last modified date and time using an HTTP POST request. This is the only supported POST request in RepRapFirmware, however be aware that the POST request is no standard HTTP request. To make this work in the firmware, the payload (ie. file) has to be send in one chunk right after the HTTP header without any encapsulation. This mechanism is used to speed up transfers. Once complete, the firmware responds with {"err":[code]}. If everything goes well, the error code will be 0 and 1 on failure.
61 |
62 | #### rr_download?name=XXX
63 | Download a specified file from the SD card.
64 |
65 | #### rr_delete?name=XXX
66 | Delete a file from the SD card. The firmware responds again with `{"err":[code]}` and the error code will be 0 on success.
67 |
68 | #### rr_filelist?dir=XXX
69 | Request a file list from the directory XXX. Unlike rr_files, which was used in past web interface versions, this request returns a JSON object which encapsulates each file in the following format:
70 |
71 | `{"type":[type],"name":"[name]","size":[size],"lastModified":"[datetime]"}`
72 |
73 | Type can be either 'd' if it is a directory or 'f' if it is a regular file. The size is reported in bytes.
74 |
75 | If an error occurs, the firmware will respond with `{"err":[code]}`. If the code is 1, the directory doesn't exist. If it is 2, the requested volume is not mounted.
76 |
77 | #### rr_fileinfo?name=XXX
78 | Parse G-code file information from file XXX or return file information about the file being printed if the key is omitted. RepRapFirmware implements a dedicate function to retrieve information from a G-code file (see also M36) which may be used on the G-code file list and on the print status page.
79 |
80 | #### rr_move?old=XXX&new=YYY
81 | Move a file on the SD card from XXX to YYY. Returns {"err":[code]} after completion where code will be 0 if the request was successful.
82 |
83 | #### rr_mkdir?dir=XXX
84 | Create a new directory. Returns {"err":[code]} with code being 0 if the directory could be created.
85 |
86 | #### rr_config
87 | Get the configuration response. Some printer information do not need to be requested for regular usage but to obtain machine properties and firmware versions this request can be used.
88 |
89 | ## Building Duet Web Control
90 |
91 | The final file structure of a Duet Web Control package may differ from the structure in the "core" directory. For example, the Duet WiFi has a filename length limit of 32 characters, so the existing paths must be adjusted to meet this limitation. Apart from that, it may be required to compress the target files for webservers that cannot send source files in parallel. In addition, web files on the Duet are not stored in sub-directories, so the paths must be changed for this board as well.
92 |
93 | For these purposes a build script has been introduced which can be run on Linux (and possibly OS X). To do so, open a terminal in the DWC root directory and run `./build.sh`. Refer to the build script header to see which other tools you will need.
94 |
95 | Once the script has completed, you should get two files:
96 |
97 | - DuetWebControl-$VERSION.bin (SPIFFS image for Duet WiFi)
98 | - DuetWebControl-$VERSION.zip (ZIP package for first-generation Duets)
99 |
100 | These packages can be uploaded via Duet Web Control to update the web interface. Due to the extra compression on the Duet WiFi, it is recommended to test new features on first-generation Duets first.
101 |
102 | ## Internationalization
103 |
104 | Duet Web Control is capable of translating basically every text to a custom language. The translated entries are stored in an extra (and yet optional) XML file called "language.xml". Each language has its own section and if you want to add support for your own language, just follow the following tasks:
105 |
106 | 1. Copy the first section containing the German translations, i.e. the whole text between and and paste it before the last line of the file. It is explicitly recommended to use this section, because it will be up-to-date on every official release.
107 | 2. Change "de" to your own country code and replace "Deutsch" with your own language.
108 | 3. Replace the content of each "string" tag with your own translated text. Dynamic arguments may be specified in curly braces as in "Uploading File(s), {0}% Complete".
109 |
110 | If your language is supported, but you are missing entries for your own language, you can easily extend the existing translations. The list of translations is sequential, so you can always compare your own language section with the "de" language section. To extend them, check the length of your own language section, copy the missing entries from the "de" tag to your own language section and update the missing translations. In case some texts are not covered by the German translations, you can always create your own `...` tags, too.
111 |
112 | When you are done and would like to contribute your changes, feel free to send a pull request on GitHub or send me your updated language.xml file via e-mail.
113 |
--------------------------------------------------------------------------------
/core/js/3rd-party/jquery.flot.navigate.js:
--------------------------------------------------------------------------------
1 | /* Flot plugin for adding the ability to pan and zoom the plot.
2 |
3 | Copyright (c) 2007-2014 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | The default behaviour is double click and scrollwheel up/down to zoom in, drag
7 | to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and
8 | plot.pan( offset ) so you easily can add custom controls. It also fires
9 | "plotpan" and "plotzoom" events, useful for synchronizing plots.
10 |
11 | The plugin supports these options:
12 |
13 | zoom: {
14 | interactive: false
15 | trigger: "dblclick" // or "click" for single click
16 | amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
17 | }
18 |
19 | pan: {
20 | interactive: false
21 | cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer"
22 | frameRate: 20
23 | }
24 |
25 | xaxis, yaxis, x2axis, y2axis: {
26 | zoomRange: null // or [ number, number ] (min range, max range) or false
27 | panRange: null // or [ number, number ] (min, max) or false
28 | }
29 |
30 | "interactive" enables the built-in drag/click behaviour. If you enable
31 | interactive for pan, then you'll have a basic plot that supports moving
32 | around; the same for zoom.
33 |
34 | "amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to
35 | the current viewport.
36 |
37 | "cursor" is a standard CSS mouse cursor string used for visual feedback to the
38 | user when dragging.
39 |
40 | "frameRate" specifies the maximum number of times per second the plot will
41 | update itself while the user is panning around on it (set to null to disable
42 | intermediate pans, the plot will then not update until the mouse button is
43 | released).
44 |
45 | "zoomRange" is the interval in which zooming can happen, e.g. with zoomRange:
46 | [1, 100] the zoom will never scale the axis so that the difference between min
47 | and max is smaller than 1 or larger than 100. You can set either end to null
48 | to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis
49 | will be disabled.
50 |
51 | "panRange" confines the panning to stay within a range, e.g. with panRange:
52 | [-10, 20] panning stops at -10 in one end and at 20 in the other. Either can
53 | be null, e.g. [-10, null]. If you set panRange to false, panning on that axis
54 | will be disabled.
55 |
56 | Example API usage:
57 |
58 | plot = $.plot(...);
59 |
60 | // zoom default amount in on the pixel ( 10, 20 )
61 | plot.zoom({ center: { left: 10, top: 20 } });
62 |
63 | // zoom out again
64 | plot.zoomOut({ center: { left: 10, top: 20 } });
65 |
66 | // zoom 200% in on the pixel (10, 20)
67 | plot.zoom({ amount: 2, center: { left: 10, top: 20 } });
68 |
69 | // pan 100 pixels to the left and 20 down
70 | plot.pan({ left: -100, top: 20 })
71 |
72 | Here, "center" specifies where the center of the zooming should happen. Note
73 | that this is defined in pixel space, not the space of the data points (you can
74 | use the p2c helpers on the axes in Flot to help you convert between these).
75 |
76 | "amount" is the amount to zoom the viewport relative to the current range, so
77 | 1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You
78 | can set the default in the options.
79 |
80 | */
81 |
82 | // First two dependencies, jquery.event.drag.js and
83 | // jquery.mousewheel.js, we put them inline here to save people the
84 | // effort of downloading them.
85 |
86 | /*
87 | jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
88 | Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
89 | */
90 | (function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) {
245 | // make sure min < max
246 | var tmp = min;
247 | min = max;
248 | max = tmp;
249 | }
250 |
251 | //Check that we are in panRange
252 | if (pr) {
253 | if (pr[0] != null && min < pr[0]) {
254 | min = pr[0];
255 | }
256 | if (pr[1] != null && max > pr[1]) {
257 | max = pr[1];
258 | }
259 | }
260 |
261 | var range = max - min;
262 | if (zr &&
263 | ((zr[0] != null && range < zr[0] && amount >1) ||
264 | (zr[1] != null && range > zr[1] && amount <1)))
265 | return;
266 |
267 | opts.min = min;
268 | opts.max = max;
269 | });
270 |
271 | plot.setupGrid();
272 | plot.draw();
273 |
274 | if (!args.preventEvent)
275 | plot.getPlaceholder().trigger("plotzoom", [ plot, args ]);
276 | };
277 |
278 | plot.pan = function (args) {
279 | var delta = {
280 | x: +args.left,
281 | y: +args.top
282 | };
283 |
284 | if (isNaN(delta.x))
285 | delta.x = 0;
286 | if (isNaN(delta.y))
287 | delta.y = 0;
288 |
289 | $.each(plot.getAxes(), function (_, axis) {
290 | var opts = axis.options,
291 | min, max, d = delta[axis.direction];
292 |
293 | min = axis.c2p(axis.p2c(axis.min) + d),
294 | max = axis.c2p(axis.p2c(axis.max) + d);
295 |
296 | var pr = opts.panRange;
297 | if (pr === false) // no panning on this axis
298 | return;
299 |
300 | if (pr) {
301 | // check whether we hit the wall
302 | if (pr[0] != null && pr[0] > min) {
303 | d = pr[0] - min;
304 | min += d;
305 | max += d;
306 | }
307 |
308 | if (pr[1] != null && pr[1] < max) {
309 | d = pr[1] - max;
310 | min += d;
311 | max += d;
312 | }
313 | }
314 |
315 | opts.min = min;
316 | opts.max = max;
317 | });
318 |
319 | plot.setupGrid();
320 | plot.draw();
321 |
322 | if (!args.preventEvent)
323 | plot.getPlaceholder().trigger("plotpan", [ plot, args ]);
324 | };
325 |
326 | function shutdown(plot, eventHolder) {
327 | eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick);
328 | eventHolder.unbind("mousewheel", onMouseWheel);
329 | eventHolder.unbind("dragstart", onDragStart);
330 | eventHolder.unbind("drag", onDrag);
331 | eventHolder.unbind("dragend", onDragEnd);
332 | if (panTimeout)
333 | clearTimeout(panTimeout);
334 | }
335 |
336 | plot.hooks.bindEvents.push(bindEvents);
337 | plot.hooks.shutdown.push(shutdown);
338 | }
339 |
340 | $.plot.plugins.push({
341 | init: init,
342 | options: options,
343 | name: 'navigate',
344 | version: '1.3'
345 | });
346 | })(jQuery);
347 |
--------------------------------------------------------------------------------
/core/js/3rd-party/bootstrap-notify.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Project: Bootstrap Notify = v3.1.5
3 | * Description: Turns standard Bootstrap alerts into "Growl-like" notifications.
4 | * Author: Mouse0270 aka Robert McIntosh
5 | * License: MIT License
6 | * Website: https://github.com/mouse0270/bootstrap-growl
7 | */
8 |
9 | /* global define:false, require: false, jQuery:false */
10 |
11 | (function (factory) {
12 | if (typeof define === 'function' && define.amd) {
13 | // AMD. Register as an anonymous module.
14 | define(['jquery'], factory);
15 | } else if (typeof exports === 'object') {
16 | // Node/CommonJS
17 | factory(require('jquery'));
18 | } else {
19 | // Browser globals
20 | factory(jQuery);
21 | }
22 | }(function ($) {
23 | // Create the defaults once
24 | var defaults = {
25 | element: 'body',
26 | position: null,
27 | type: "info",
28 | allow_dismiss: true,
29 | allow_duplicates: true,
30 | newest_on_top: false,
31 | showProgressbar: false,
32 | placement: {
33 | from: "top",
34 | align: "right"
35 | },
36 | offset: 20,
37 | spacing: 10,
38 | z_index: 1031,
39 | delay: 5000,
40 | timer: 1000,
41 | url_target: '_blank',
42 | mouse_over: null,
43 | animate: {
44 | enter: 'animated fadeInDown',
45 | exit: 'animated fadeOutUp'
46 | },
47 | onShow: null,
48 | onShown: null,
49 | onClose: null,
50 | onClosed: null,
51 | onClick: null,
52 | icon_type: 'class',
53 | template: '
{1}{2}
'
54 | };
55 |
56 | String.format = function () {
57 | var args = arguments;
58 | var str = arguments[0];
59 | return str.replace(/(\{\{\d\}\}|\{\d\})/g, function (str) {
60 | if (str.substring(0, 2) === "{{") return str;
61 | var num = parseInt(str.match(/\d/)[0]);
62 | return args[num + 1];
63 | });
64 | };
65 |
66 | function isDuplicateNotification(notification) {
67 | var isDupe = false;
68 |
69 | $('[data-notify="container"]').each(function (i, el) {
70 | var $el = $(el);
71 | var title = $el.find('[data-notify="title"]').html().trim();
72 | var message = $el.find('[data-notify="message"]').html().trim();
73 |
74 | // The input string might be different than the actual parsed HTML string!
75 | // ( vs for example)
76 | // So we have to force-parse this as HTML here!
77 | var isSameTitle = title === $("
" + notification.settings.content.title + "
").html().trim();
78 | var isSameMsg = message === $("
" + notification.settings.content.message + "
").html().trim();
79 | var isSameType = $el.hasClass('alert-' + notification.settings.type);
80 |
81 | if (isSameTitle && isSameMsg && isSameType) {
82 | //we found the dupe. Set the var and stop checking.
83 | isDupe = true;
84 | }
85 | return !isDupe;
86 | });
87 |
88 | return isDupe;
89 | }
90 |
91 | function Notify(element, content, options) {
92 | // Setup Content of Notify
93 | var contentObj = {
94 | content: {
95 | message: typeof content === 'object' ? content.message : content,
96 | title: content.title ? content.title : '',
97 | icon: content.icon ? content.icon : '',
98 | url: content.url ? content.url : '#',
99 | target: content.target ? content.target : '-'
100 | }
101 | };
102 |
103 | options = $.extend(true, {}, contentObj, options);
104 | this.settings = $.extend(true, {}, defaults, options);
105 | this._defaults = defaults;
106 | if (this.settings.content.target === "-") {
107 | this.settings.content.target = this.settings.url_target;
108 | }
109 | this.animations = {
110 | start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart',
111 | end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend'
112 | };
113 |
114 | if (typeof this.settings.offset === 'number') {
115 | this.settings.offset = {
116 | x: this.settings.offset,
117 | y: this.settings.offset
118 | };
119 | }
120 |
121 | //if duplicate messages are not allowed, then only continue if this new message is not a duplicate of one that it already showing
122 | if (this.settings.allow_duplicates || (!this.settings.allow_duplicates && !isDuplicateNotification(this))) {
123 | this.init();
124 | }
125 | }
126 |
127 | $.extend(Notify.prototype, {
128 | init: function () {
129 | var self = this;
130 |
131 | this.buildNotify();
132 | if (this.settings.content.icon) {
133 | this.setIcon();
134 | }
135 | if (this.settings.content.url != "#") {
136 | this.styleURL();
137 | }
138 | this.styleDismiss();
139 | this.placement();
140 | this.bind();
141 |
142 | this.notify = {
143 | $ele: this.$ele,
144 | update: function (command, update) {
145 | var commands = {};
146 | if (typeof command === "string") {
147 | commands[command] = update;
148 | } else {
149 | commands = command;
150 | }
151 | for (var cmd in commands) {
152 | switch (cmd) {
153 | case "type":
154 | this.$ele.removeClass('alert-' + self.settings.type);
155 | this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type);
156 | self.settings.type = commands[cmd];
157 | this.$ele.addClass('alert-' + commands[cmd]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[cmd]);
158 | break;
159 | case "icon":
160 | var $icon = this.$ele.find('[data-notify="icon"]');
161 | if (self.settings.icon_type.toLowerCase() === 'class') {
162 | $icon.removeClass(self.settings.content.icon).addClass(commands[cmd]);
163 | } else {
164 | if (!$icon.is('img')) {
165 | $icon.find('img');
166 | }
167 | $icon.attr('src', commands[cmd]);
168 | }
169 | self.settings.content.icon = commands[command];
170 | break;
171 | case "progress":
172 | var newDelay = self.settings.delay - (self.settings.delay * (commands[cmd] / 100));
173 | this.$ele.data('notify-delay', newDelay);
174 | this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[cmd]).css('width', commands[cmd] + '%');
175 | break;
176 | case "url":
177 | this.$ele.find('[data-notify="url"]').attr('href', commands[cmd]);
178 | break;
179 | case "target":
180 | this.$ele.find('[data-notify="url"]').attr('target', commands[cmd]);
181 | break;
182 | default:
183 | this.$ele.find('[data-notify="' + cmd + '"]').html(commands[cmd]);
184 | }
185 | }
186 | var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y);
187 | self.reposition(posX);
188 | },
189 | close: function () {
190 | self.close();
191 | }
192 | };
193 |
194 | },
195 | buildNotify: function () {
196 | var content = this.settings.content;
197 | this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target));
198 | this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align);
199 | if (!this.settings.allow_dismiss) {
200 | this.$ele.find('[data-notify="dismiss"]').css('display', 'none');
201 | }
202 | if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) {
203 | this.$ele.find('[data-notify="progressbar"]').remove();
204 | }
205 | },
206 | setIcon: function () {
207 | if (this.settings.icon_type.toLowerCase() === 'class') {
208 | this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon);
209 | } else {
210 | if (this.$ele.find('[data-notify="icon"]').is('img')) {
211 | this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon);
212 | } else {
213 | this.$ele.find('[data-notify="icon"]').append('');
214 | }
215 | }
216 | },
217 | styleDismiss: function () {
218 | this.$ele.find('[data-notify="dismiss"]').css({
219 | position: 'absolute',
220 | right: '10px',
221 | top: '5px',
222 | zIndex: this.settings.z_index + 2
223 | });
224 | },
225 | styleURL: function () {
226 | this.$ele.find('[data-notify="url"]').css({
227 | backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)',
228 | height: '100%',
229 | left: 0,
230 | position: 'absolute',
231 | top: 0,
232 | width: '100%',
233 | zIndex: this.settings.z_index + 1
234 | });
235 | },
236 | placement: function () {
237 | var self = this,
238 | offsetAmt = this.settings.offset.y,
239 | css = {
240 | display: 'inline-block',
241 | margin: '0px auto',
242 | position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'),
243 | transition: 'all .5s ease-in-out',
244 | zIndex: this.settings.z_index
245 | },
246 | hasAnimation = false,
247 | settings = this.settings;
248 |
249 | $('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function () {
250 | offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing));
251 | });
252 | if (this.settings.newest_on_top === true) {
253 | offsetAmt = this.settings.offset.y;
254 | }
255 | css[this.settings.placement.from] = offsetAmt + 'px';
256 |
257 | switch (this.settings.placement.align) {
258 | case "left":
259 | case "right":
260 | css[this.settings.placement.align] = this.settings.offset.x + 'px';
261 | break;
262 | case "center":
263 | css.left = 0;
264 | css.right = 0;
265 | break;
266 | }
267 | this.$ele.css(css).addClass(this.settings.animate.enter);
268 | $.each(Array('webkit-', 'moz-', 'o-', 'ms-', ''), function (index, prefix) {
269 | self.$ele[0].style[prefix + 'AnimationIterationCount'] = 1;
270 | });
271 |
272 | $(this.settings.element).append(this.$ele);
273 |
274 | if (this.settings.newest_on_top === true) {
275 | offsetAmt = (parseInt(offsetAmt) + parseInt(this.settings.spacing)) + this.$ele.outerHeight();
276 | this.reposition(offsetAmt);
277 | }
278 |
279 | if ($.isFunction(self.settings.onShow)) {
280 | self.settings.onShow.call(this.$ele);
281 | }
282 |
283 | this.$ele.one(this.animations.start, function () {
284 | hasAnimation = true;
285 | }).one(this.animations.end, function () {
286 | self.$ele.removeClass(self.settings.animate.enter);
287 | if ($.isFunction(self.settings.onShown)) {
288 | self.settings.onShown.call(this);
289 | }
290 | });
291 |
292 | setTimeout(function () {
293 | if (!hasAnimation) {
294 | if ($.isFunction(self.settings.onShown)) {
295 | self.settings.onShown.call(this);
296 | }
297 | }
298 | }, 600);
299 | },
300 | bind: function () {
301 | var self = this;
302 |
303 | this.$ele.find('[data-notify="dismiss"]').on('click', function () {
304 | self.close();
305 | });
306 |
307 | if ($.isFunction(self.settings.onClick)) {
308 | this.$ele.on('click', function (event) {
309 | if (event.target != self.$ele.find('[data-notify="dismiss"]')[0]) {
310 | self.settings.onClick.call(this, event);
311 | }
312 | });
313 | }
314 |
315 | this.$ele.mouseover(function () {
316 | $(this).data('data-hover', "true");
317 | }).mouseout(function () {
318 | $(this).data('data-hover', "false");
319 | });
320 | this.$ele.data('data-hover', "false");
321 |
322 | if (this.settings.delay > 0) {
323 | self.$ele.data('notify-delay', self.settings.delay);
324 | var timer = setInterval(function () {
325 | var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer;
326 | if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over === "pause") || self.settings.mouse_over != "pause") {
327 | var percent = ((self.settings.delay - delay) / self.settings.delay) * 100;
328 | self.$ele.data('notify-delay', delay);
329 | self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%');
330 | }
331 | if (delay <= -(self.settings.timer)) {
332 | clearInterval(timer);
333 | self.close();
334 | }
335 | }, self.settings.timer);
336 | }
337 | },
338 | close: function () {
339 | var self = this,
340 | posX = parseInt(this.$ele.css(this.settings.placement.from)),
341 | hasAnimation = false;
342 |
343 | this.$ele.attr('data-closing', 'true').addClass(this.settings.animate.exit);
344 | self.reposition(posX);
345 |
346 | if ($.isFunction(self.settings.onClose)) {
347 | self.settings.onClose.call(this.$ele);
348 | }
349 |
350 | this.$ele.one(this.animations.start, function () {
351 | hasAnimation = true;
352 | }).one(this.animations.end, function () {
353 | $(this).remove();
354 | if ($.isFunction(self.settings.onClosed)) {
355 | self.settings.onClosed.call(this);
356 | }
357 | });
358 |
359 | setTimeout(function () {
360 | if (!hasAnimation) {
361 | self.$ele.remove();
362 | if ($.isFunction(self.settings.onClosed)) {
363 | self.settings.onClosed.call(this);
364 | }
365 | }
366 | }, 600);
367 | },
368 | reposition: function (posX) {
369 | var self = this,
370 | notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])',
371 | $elements = this.$ele.nextAll(notifies);
372 | if (this.settings.newest_on_top === true) {
373 | $elements = this.$ele.prevAll(notifies);
374 | }
375 | $elements.each(function () {
376 | $(this).css(self.settings.placement.from, posX);
377 | posX = (parseInt(posX) + parseInt(self.settings.spacing)) + $(this).outerHeight();
378 | });
379 | }
380 | });
381 |
382 | $.notify = function (content, options) {
383 | var plugin = new Notify(this, content, options);
384 | return plugin.notify;
385 | };
386 | $.notifyDefaults = function (options) {
387 | defaults = $.extend(true, {}, defaults, options);
388 | return defaults;
389 | };
390 |
391 | $.notifyClose = function (selector) {
392 |
393 | if (typeof selector === "undefined" || selector === "all") {
394 | $('[data-notify]').find('[data-notify="dismiss"]').trigger('click');
395 | }else if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){
396 | $('.alert-' + selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click');
397 | } else if(selector){
398 | $(selector + '[data-notify]').find('[data-notify="dismiss"]').trigger('click');
399 | }
400 | else {
401 | $('[data-notify-position="' + selector + '"]').find('[data-notify="dismiss"]').trigger('click');
402 | }
403 | };
404 |
405 | $.notifyCloseExcept = function (selector) {
406 |
407 | if(selector === 'success' || selector === 'info' || selector === 'warning' || selector === 'danger'){
408 | $('[data-notify]').not('.alert-' + selector).find('[data-notify="dismiss"]').trigger('click');
409 | } else{
410 | $('[data-notify]').not(selector).find('[data-notify="dismiss"]').trigger('click');
411 | }
412 | };
413 |
414 |
415 | }));
416 |
417 |
418 |
--------------------------------------------------------------------------------
/core/js/modals.js:
--------------------------------------------------------------------------------
1 | /* Modal dialog functions for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 |
10 | /* Generic workarounds */
11 |
12 | $(".modal").on("hidden.bs.modal", function() {
13 | // Bootstrap bug: Padding is added to the right, but never cleaned
14 | $("body").css("padding-right", "");
15 | });
16 |
17 |
18 | /* Confirmation Dialog */
19 |
20 | function showConfirmationDialog(title, message, callback) {
21 | $("#modal_confirmation h4").html(' ' + title);
22 | $("#modal_confirmation p").html(message);
23 | $("#modal_confirmation button.btn-success").off().one("click", callback);
24 | $("#modal_confirmation").modal("show");
25 | $("#modal_confirmation .btn-success").focus();
26 | }
27 |
28 |
29 | /* Text Input Dialog */
30 |
31 | function showTextInput(title, message, callback, text, emptyCallback) {
32 | $("#modal_textinput h4").html(title);
33 | $("#modal_textinput p").html(message);
34 | $("#modal_textinput input").val((text == undefined) ? "" : text);
35 | $("#modal_textinput form").off().submit(function(e) {
36 | $("#modal_textinput").modal("hide");
37 | var value = $("#modal_textinput input").val();
38 | if (value.trim() != "") {
39 | callback(value);
40 | } else if (emptyCallback != undefined) {
41 | emptyCallback();
42 | }
43 | e.preventDefault();
44 | });
45 | $("#modal_textinput").modal("show");
46 | }
47 |
48 | $("#modal_textinput").on("shown.bs.modal", function() {
49 | $("#modal_textinput input").focus();
50 | });
51 |
52 |
53 | /* Change Step Dialog */
54 |
55 | var stepChangeType, stepChangeIndex, stepChangeAxisIndex;
56 |
57 | function showStepDialog(type, index, currentValue, axisIndex) {
58 | stepChangeType = type;
59 | stepChangeIndex = index;
60 | stepChangeAxisIndex = axisIndex;
61 |
62 | $("#input_step_amount").val(currentValue);
63 | $("#input_step_amount_unit").text((type == "feedrate") ? T("mm/s") : T("mm"));
64 | $("#modal_change_step").modal("show");
65 | }
66 |
67 | $("#modal_change_step").on("shown.bs.modal", function() {
68 | $("#modal_change_step input").focus();
69 | });
70 |
71 | $("#modal_change_step form").submit(function(e) {
72 | if (stepChangeType == "axis") {
73 | settings.axisMoveSteps[stepChangeAxisIndex][stepChangeIndex] = $("#input_step_amount").val();
74 | } else if (stepChangeType == "amount") {
75 | settings.extruderAmounts[stepChangeIndex] = $("#input_step_amount").val();
76 | } else {
77 | settings.extruderFeedrates[stepChangeIndex] = $("#input_step_amount").val();
78 | }
79 |
80 | applyMovementSteps();
81 | $("#modal_change_step").modal("hide");
82 |
83 | e.preventDefault();
84 | });
85 |
86 |
87 | /* Host Prompt */
88 |
89 | function showHostPrompt() {
90 | $('#input_host').val(getLocalSetting("lastHost", ""));
91 | $("#modal_host_input").modal("show");
92 | }
93 |
94 | $("#modal_host_input").on("shown.bs.modal", function() {
95 | $("#input_host").focus();
96 | });
97 |
98 | $("#form_host").submit(function(e) {
99 | $("#modal_host_input").off("hide.bs.modal").modal("hide");
100 | ajaxPrefix = settings.lastHost = $("#input_host").val(); // let the user decide if this shall be saved
101 |
102 | ajaxPrefix += "/";
103 | if (ajaxPrefix.indexOf("://") == -1) {
104 | // Prepend http prefix if no URI scheme is given
105 | ajaxPrefix = "http://" + ajaxPrefix;
106 | }
107 |
108 | connect(sessionPassword, false);
109 | e.preventDefault();
110 | });
111 |
112 |
113 | /* Password prompt */
114 |
115 | function showPasswordPrompt() {
116 | $('#input_password').val("");
117 | $("#modal_pass_input").modal("show");
118 | }
119 |
120 | $("#form_password").submit(function(e) {
121 | $("#modal_pass_input").off("hide.bs.modal").modal("hide");
122 | connect($("#input_password").val(), false);
123 | e.preventDefault();
124 | });
125 |
126 | $("#modal_pass_input").on("shown.bs.modal", function() {
127 | $("#input_password").focus();
128 | });
129 |
130 |
131 | /* Filament Change Dialog */
132 |
133 | var filamentChangeTool, changingFilament;
134 |
135 | function showFilamentDialog(tool, changeFilament) {
136 | // make list of all available filaments
137 | $("#div_filaments").children().remove();
138 | $("#table_filaments > tbody > tr").each(function() {
139 | var filament = $(this).data("filament");
140 | var isLoaded = false;
141 | for(var i = 0; i < toolMapping.length; i++) {
142 | if (toolMapping[i].hasOwnProperty("filament") && toolMapping[i].filament == filament) {
143 | isLoaded = true;
144 | break;
145 | }
146 | }
147 |
148 | if (!isLoaded) {
149 | $("#div_filaments").append(' ' + filament + '');
150 | }
151 | });
152 |
153 | // show notification or selection dialog
154 | if ($("#div_filaments").children().length == 0) {
155 | showMessage("warning", T("No Filaments"), T("There are no other filaments available to choose. Please go to the Filaments page and define more."));
156 | } else {
157 | filamentChangeTool = tool;
158 | changingFilament = changeFilament;
159 | $("#modal_change_filament").modal("show");
160 | }
161 | }
162 |
163 | $("body").on("click", ".a-load-filament", function(e) {
164 | $("#modal_change_filament").modal("hide");
165 |
166 | var gcode = "";
167 | if (lastStatusResponse != undefined && lastStatusResponse.currentTool != filamentChangeTool) {
168 | gcode = "T" + filamentChangeTool + "\n";
169 | }
170 | if (changingFilament) {
171 | gcode += "M702\n";
172 | }
173 | gcode += "M701 S\"" + $(this).data("filament") + "\"";
174 | if (!compatibilityMode) { gcode += "\nM703"; }
175 | sendGCode(gcode);
176 |
177 | e.preventDefault();
178 | });
179 |
180 |
181 | /* File Edit Dialog */
182 |
183 | function showEditDialog(title, content, callback) {
184 | $("#modal_edit .modal-title").text(T("Editing {0}", title));
185 | var edit = $("#modal_edit textarea").val(content).get(0);
186 | edit.selectionStart = edit.selectionEnd = 0;
187 | $("#modal_edit").modal("show");
188 | $("#btn_save_file").off("click").click(function() {
189 | $("#modal_edit").modal("hide");
190 | callback($("#modal_edit textarea").val());
191 | });
192 | }
193 |
194 | $("#modal_edit").on("shown.bs.modal", function() {
195 | $("#modal_edit textarea").focus();
196 | });
197 |
198 | $("#modal_edit div.modal-content").resize(function() {
199 | var contentHeight = $(this).height();
200 | var headerHeight = $("#modal_edit div.modal-header").height();
201 | var footerHeight = $("#modal_edit div.modal-footer").height();
202 | $("#modal_edit div.modal-body").css("height", (contentHeight - headerHeight - footerHeight - 60) + "px");
203 | });
204 |
205 | $(document).delegate("#modal_edit textarea", "keydown", function(e) {
206 | var keyCode = e.keyCode || e.which;
207 |
208 | if (keyCode == 9) {
209 | e.preventDefault();
210 | var start = $(this).get(0).selectionStart;
211 | var end = $(this).get(0).selectionEnd;
212 |
213 | // set textarea value to: text before caret + tab + text after caret
214 | $(this).val($(this).val().substring(0, start)
215 | + "\t"
216 | + $(this).val().substring(end));
217 |
218 | // put caret at right position again
219 | $(this).get(0).selectionStart = $(this).get(0).selectionEnd = start + 1;
220 | }
221 | });
222 |
223 |
224 | /* Start Scan Dialog (proprietary) */
225 |
226 | $("#btn_start_scan").click(function() {
227 | if (!$(this).hasClass("disabled")) {
228 | if (vendor == "diabase") {
229 | // Properietary implemenation with extra steps
230 | $("#modal_start_scan").modal("show");
231 | } else {
232 | // Basic open-source variant
233 | showTextInput(T("Start new 3D scan"), T("Please enter a name for the new scan:"), function(name) {
234 | if (filenameValid(name)) {
235 | // Let the firmware do the communication to the board
236 | sendGCode("M752 S360 P" + name);
237 | } else {
238 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes."));
239 | }
240 | }, undefined, function() {
241 | showMessage("danger", T("Error"), T("The filename for a new scan must not be empty!"));
242 | });
243 | }
244 | }
245 | });
246 |
247 | $("input[name='scanMode']").change(function() {
248 | $(".scan-unit").text(($("input[name='scanMode']:checked").val() == 0) ? T("mm") : T("°"));
249 | });
250 |
251 | $("#modal_start_scan input").keyup(function() {
252 | $("#btn_start_scan_modal").toggleClass("disabled", $("#modal_start_scan input:invalid").length > 0);
253 | });
254 |
255 | $("#btn_toggle_laser").click(function(e) {
256 | if ($(this).hasClass("active")) {
257 | sendGCode("M755 P0");
258 | $(this).removeClass("active").children("span.content").text(T("Activate Laser"));
259 | } else {
260 | sendGCode("M755 P1")
261 | $(this).addClass("active").children("span.content").text(T("Deactivate Laser"));
262 | }
263 |
264 | $(this).blur();
265 | e.preventDefault();
266 | });
267 |
268 | $("#modal_start_scan form").submit(function(e) {
269 | if (!$("#btn_start_scan_modal").hasClass("disabled")) {
270 | // 1. Turn off the alignment laser if it is still active
271 | if ($("#btn_toggle_laser").hasClass("active")) {
272 | $("#btn_toggle_laser").removeClass("active").children("span.content").text(T("Activate Laser"));
273 | if (isConnected) {
274 | sendGCode("M755 P0");
275 | }
276 | }
277 |
278 | // 2. Start a new scan
279 | var filename = $("#input_scan_filename").val();
280 | var range = $("#input_scan_range").val();
281 | var resolution = $("#input_scan_resolution").val();
282 | var mode = $("input[name='scanMode']:checked").val();
283 |
284 | // 3. Check the filename and start a new scan
285 | if (filenameValid(filename)) {
286 | sendGCode("M752 S" + range + " R" + resolution + " N" + mode + " P\"" + filename + "\"");
287 | } else {
288 | showMessage("danger", T("Error"), T("The specified filename is invalid. It may not contain quotes, colons or (back)slashes."));
289 | }
290 |
291 | // 4. Hide the modal dialog
292 | $("#modal_start_scan").modal("hide");
293 | }
294 |
295 | e.preventDefault();
296 | });
297 |
298 | $("#modal_start_scan").on("hidden.bs.modal", function() {
299 | if ($("#btn_toggle_laser").hasClass("active")) {
300 | $("#btn_toggle_laser").removeClass("active").children("span.content").text(T("Activate Laser"));
301 | if (isConnected) {
302 | // Turn off laser again when the dialog is closed
303 | sendGCode("M755 P0");
304 | }
305 | }
306 | });
307 |
308 |
309 | /* Scanner Progress Dialogs */
310 |
311 | function updateScannerDialogs(scanResponse) {
312 | var scanProgress = 100, postProcessingProgress = 100, uploadProgress = 100;
313 |
314 | if (scanResponse.status == "S" || scanResponse.status == "P" || scanResponse.status == "U") {
315 | // Scanner is active
316 | if (!$("#modal_scanner").hasClass("in")) {
317 | $("#btn_cancel_scan").removeClass("hidden").removeClass("disabled");
318 | $("#btn_close_scan").addClass("hidden");
319 |
320 | $("#modal_scanner .modal-title").text(T("Scanning..."));
321 | $("#p_scan_info").text(T("Please wait while a scan is being made. This may take a while..."));
322 |
323 | $("#modal_scanner").modal("show");
324 | }
325 |
326 | // Update progress
327 | if (scanResponse.status == "S") {
328 | scanProgress = scanResponse.progress;
329 | postProcessingProgress = 0;
330 | uploadProgress = 0;
331 | } else if (scanResponse.status == "P") {
332 | scanProgress = 100;
333 | postProcessingProgress = scanResponse.progress;
334 | uploadProgress = 0;
335 | } else if (scanResponse.status == "U") {
336 | scanProgress = 100;
337 | postProcessingProgress = 100;
338 | uploadProgress = scanResponse.progress;
339 | $("#btn_cancel_scan").addClass("disabled");
340 | }
341 | } else if (scanResponse.status == "C") {
342 | // Scanner calibration is running
343 | if (!$("#modal_scanner_calibration").hasClass("in")) {
344 | $("#btn_cancel_calibration").removeClass("disabled");
345 | $("#modal_scanner_calibration").modal("show");
346 | }
347 |
348 | // Update progress
349 | $("#progress_calibration").css("width", scanResponse.progress + "%");
350 | $("#span_calibration_progress").text(T("{0} %", scanResponse.progress));
351 | } else if (scanResponse.status == "I") {
352 | // Scanner is inactive
353 | if ($("#modal_scanner").hasClass("in") && $("#modal_scanner .modal-title").text() != T("Scan complete")) {
354 | $("#modal_scanner .modal-title").text(T("Scan complete"));
355 | $("#p_scan_info").text(T("Your 3D scan is now complete! You may download it from the file list next."));
356 |
357 | $("#btn_cancel_scan").addClass("hidden");
358 | $("#btn_close_scan").removeClass("hidden");
359 |
360 | updateScanFiles();
361 | $(".span-refresh-scans").addClass("hidden");
362 | }
363 |
364 | if ($("#modal_scanner_calibration").hasClass("in")) {
365 | $("#modal_scanner_calibration").modal("hide");
366 | }
367 | }
368 |
369 | // Update progress bars
370 | if ($("#modal_scanner").hasClass("in")) {
371 | $("#progress_scan").css("width", scanProgress + "%");
372 | $("#span_scan_progress").text(T("{0} %", scanProgress));
373 |
374 | $("#progress_scan_postprocessing").css("width", postProcessingProgress + "%");
375 | $("#span_scan_postprocessing_progress").text(T("{0} %", postProcessingProgress));
376 |
377 | $("#progress_scan_upload").css("width", uploadProgress + "%");
378 | $("#span_scan_upload_progress").text(T("{0} %", uploadProgress));
379 | }
380 | }
381 |
382 | $("#btn_cancel_scan").click(function() {
383 | if (!$(this).hasClass("disabled")) {
384 | sendGCode("M753");
385 | $("#modal_scanner").modal("hide");
386 | }
387 | });
388 |
389 | $("#btn_cancel_calibration").click(function() {
390 | if (!$(this).hasClass("disabled")) {
391 | sendGCode("M753");
392 | $(this).addClass("disabled");
393 | }
394 | });
395 |
396 |
397 | /* WiFi cam setup dialog (OEM) */
398 |
399 | $("#modal_wifi_cam form").submit(function(e) {
400 | sendGCode("M118 P1 S\"WIFI " + $("#input_cam_ssid").val() + " " + $("#input_cam_password").val() + "\"");
401 | lastSentGCode = "";
402 | $("#modal_wifi_cam").modal("hide");
403 | e.preventDefault();
404 | });
405 |
406 |
407 | /* Message Box Dialog */
408 |
409 | var messageBoxResponse = undefined;
410 |
411 | function updateMessageBox(response) {
412 | var timeout = response.timeout;
413 | response.timeout = 0;
414 |
415 | var stringifiedResponse = JSON.stringify(response);
416 | if (stringifiedResponse != messageBoxResponse)
417 | {
418 | messageBoxResponse = stringifiedResponse;
419 | showMessageBox(response.msg, response.title, response.mode, timeout, response.controls);
420 | }
421 | }
422 |
423 | function closeMessageBox() {
424 | if (messageBoxResponse != undefined) {
425 | if ($("#modal_messagebox").hasClass("in")) {
426 | $("#modal_messagebox").modal("hide");
427 | }
428 | messageBoxResponse = undefined;
429 | }
430 | }
431 |
432 |
433 | var messageBoxTimer = undefined;
434 |
435 | function showMessageBox(message, title, mode, timeout, controls) {
436 | // Display message, title and optionally show Z controls
437 | $("#h3_messagebox").html(message);
438 | $("#h4_messagebox_title").html(title);
439 | $("#modal_messagebox div.modal-header").toggleClass("hidden", title == "");
440 |
441 | // Toggle axis control visibility
442 | $("#div_x_controls").toggleClass("hidden", (controls & (1 << 0)) == 0);
443 | $("#div_y_controls").toggleClass("hidden", (controls & (1 << 1)) == 0);
444 | $("#div_z_controls").toggleClass("hidden", (controls & (1 << 2)) == 0);
445 |
446 | // Toggle button visibility
447 | $("#modal_messagebox div.modal-footer").toggleClass("hidden", mode == 0);
448 | $("#modal_messagebox [data-dismiss]").toggleClass("hidden", mode != 1);
449 | $("#btn_ack_messagebox").toggleClass("hidden", mode == 0 || mode == 1);
450 | $("#btn_cancel_messagebox").toggleClass("hidden", mode != 3);
451 |
452 | // Show message box
453 | var backdropValue = (mode != 1) ? "static" : "true";
454 | $("#modal_messagebox").modal({ backdrop: backdropValue });
455 |
456 | var data = $("#modal_messagebox").data("bs.modal");
457 | data.options.backdrop = backdropValue;
458 | $("#modal_messagebox").data("bs.modal", data);
459 |
460 | // Take care of the timeouts
461 | if (messageBoxTimer != undefined) {
462 | clearTimeout(messageBoxTimer);
463 | }
464 | if (timeout > 0) {
465 | messageBoxTimer = setTimeout(function() {
466 | messageBoxTimer = undefined;
467 | $("#modal_messagebox").modal("hide");
468 | }, timeout * 1000);
469 | }
470 | }
471 |
472 | $("#btn_ack_messagebox").click(function() {
473 | $("#modal_messagebox").modal("hide");
474 | sendGCode("M292");
475 | });
476 |
477 | $("#btn_cancel_messagebox").click(function() {
478 | $("#modal_messagebox").modal("hide");
479 | sendGCode("M292 P1");
480 | });
481 |
482 | $('#modal_messagebox').on("hide.bs.modal", function() {
483 | if (messageBoxTimer != undefined) {
484 | clearTimeout(messageBoxTimer);
485 | messageBoxTimer = undefined;
486 | }
487 |
488 | // FIXME: This is needed to ensure the backdrop always works as intended
489 | $('#modal_messagebox').removeData();
490 | });
491 |
492 | /* Set mesh grid area */
493 |
494 | $("#a_define_mesh").click(function(e) {
495 | $("#div_mesh_x, #div_mesh_y").toggleClass("hidden", geometry == "delta");
496 | $("#div_mesh_delta").toggleClass("hidden", geometry != "delta");
497 | $("#modal_define_mesh").modal("show");
498 | e.preventDefault();
499 | });
500 |
501 |
502 | $("#modal_define_mesh form").submit(function(e) {
503 | if (geometry != "delta") {
504 | var minX = $("#input_mesh_x_min").val();
505 | var maxX = $("#input_mesh_x_max").val();
506 | var spacingX = $("#input_mesh_x_spacing").val();
507 | var minY = $("#input_mesh_y_min").val();
508 | var maxY = $("#input_mesh_y_max").val();
509 | var spacingY = $("#input_mesh_y_spacing").val();
510 | sendGCode("M557 X" + minX + ":" + maxX + " Y" + minY + ":" + maxY + " S" + spacingX + ":" + spacingY);
511 | } else {
512 | var radius = $("#input_mesh_radius").val();
513 | var spacing = $("#input_mesh_spacing").val();
514 | sendGCode("M557 R" + radius + " S" + spacing);
515 | }
516 |
517 | $("#modal_define_mesh").modal("hide");
518 | e.preventDefault();
519 | });
520 |
521 | /* Probe Cylinder (OEM) */
522 |
523 | $("#a_probe_cylinder").click(function(e) {
524 | $("#modal_cylinder").modal("show");
525 | e.preventDefault();
526 | });
527 |
528 | $("#modal_cylinder form").submit(function(e) {
529 | sendGCode("G28 Z\nG92 Z" + ($("#input_cylinder_diameter").val() / 2));
530 | $("#modal_cylinder").modal("hide");
531 | e.preventDefault();
532 | });
533 |
--------------------------------------------------------------------------------
/core/js/utils.js:
--------------------------------------------------------------------------------
1 | /* Utility functions for Duet Web Control
2 | *
3 | * written by Christian Hammacher (c) 2016-2017
4 | *
5 | * licensed under the terms of the GPL v3
6 | * see http://www.gnu.org/licenses/gpl-3.0.html
7 | */
8 |
9 | var heatersInUse;
10 |
11 |
12 | /* Text formatting */
13 |
14 | function formatUploadSpeed(bytesPerSec) {
15 | if (settings.useKiB) {
16 | if (bytesPerSec > 1073741824) { // GiB
17 | return (bytesPerSec / 1073741824).toFixed(2) + " GiB/s";
18 | }
19 | if (bytesPerSec > 1048576) { // MiB
20 | return (bytesPerSec / 1048576).toFixed(2) + " MiB/s";
21 | }
22 | if (bytesPerSec > 1024) { // KiB
23 | return (bytesPerSec / 1024).toFixed(1) + " KiB/s";
24 | }
25 | } else {
26 | if (bytesPerSec > 1000000000) { // GB
27 | return (bytesPerSec / 1000000000).toFixed(2) + " GB/s";
28 | }
29 | if (bytesPerSec > 1000000) { // MB
30 | return (bytesPerSec / 1000000).toFixed(2) + " MB/s";
31 | }
32 | if (bytesPerSec > 1000) { // KB
33 | return (bytesPerSec / 1000).toFixed(1) + " KB/s";
34 | }
35 | }
36 | return bytesPerSec.toFixed(1) + " B/s";
37 | }
38 |
39 | function formatSize(bytes) {
40 | if (settings.useKiB) {
41 | if (bytes > 1073741824) { // GiB
42 | return (bytes / 1073741824).toFixed(1) + " GiB";
43 | }
44 | if (bytes > 1048576) { // MiB
45 | return (bytes / 1048576).toFixed(1) + " MiB";
46 | }
47 | if (bytes > 1024) { // KiB
48 | return (bytes / 1024).toFixed(1) + " KiB";
49 | }
50 | } else {
51 | if (bytes > 1000000000) { // GB
52 | return (bytes / 1000000000).toFixed(1) + " GB";
53 | }
54 | if (bytes > 1000000) { // MB
55 | return (bytes / 1000000).toFixed(1) + " MB";
56 | }
57 | if (bytes > 1000) { // KB
58 | return (bytes / 1000).toFixed(1) + " KB";
59 | }
60 | }
61 | return bytes + " B";
62 | }
63 |
64 | function formatTime(value) {
65 | value = Math.round(value);
66 | if (value < 0) {
67 | value = 0;
68 | }
69 |
70 | var timeLeft = [], temp;
71 | if (value >= 3600) {
72 | temp = Math.floor(value / 3600);
73 | if (temp > 0) {
74 | timeLeft.push(temp + "h");
75 | value = value % 3600;
76 | }
77 | }
78 | if (value >= 60) {
79 | temp = Math.floor(value / 60);
80 | if (temp > 0) {
81 | timeLeft.push((temp > 9 ? temp : "0" + temp) + "m");
82 | value = value % 60;
83 | }
84 | }
85 | value = value.toFixed(0);
86 | timeLeft.push((value > 9 ? value : "0" + value) + "s");
87 |
88 | return timeLeft.reduce(function(a, b) { return a + " " + b; });
89 | }
90 |
91 | function timeToStr(time) {
92 | // Should return an ISO-like datetime string like "2016-10-24T15:39:09"
93 | // Cannot use toISOString() here because it doesn't output the localtime
94 | var result = "";
95 | result += time.getFullYear() + "-";
96 | result += (time.getMonth() + 1) + "-";
97 | result += time.getDate() + "T";
98 | result += time.getHours() + ":";
99 | result += time.getMinutes() + ":";
100 | result += time.getSeconds();
101 | return result;
102 | }
103 |
104 | function strToTime(str) {
105 | // Date.parse() doesn't always return correct dates.
106 | // Hence we must parse it using a regex here
107 | var re = /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)/;
108 | var results;
109 | if ((results = re.exec(str)) != null) {
110 | var date = new Date();
111 | date.setFullYear(results[1]);
112 | date.setMonth(results[2] - 1);
113 | date.setDate(results[3]);
114 | date.setHours(results[4]);
115 | date.setMinutes(results[5]);
116 | date.setSeconds(results[6]);
117 | return date;
118 | }
119 | return undefined;
120 | }
121 |
122 | function getHeaterStateText(state) {
123 | switch (state) {
124 | case 0:
125 | return T("off");
126 | case 1:
127 | return T("standby");
128 | case 2:
129 | return T("active");
130 | case 3:
131 | return T("fault");
132 | case 4:
133 | return T("tuning");
134 | }
135 | return T("n/a");
136 | }
137 |
138 |
139 | /* CSV Parsing */
140 |
141 | function parseCSV(str) {
142 | var arr = [];
143 | var quote = false; // true means we're inside a quoted field
144 |
145 | // iterate over each character, keep track of current row and column (of the returned array)
146 | for (var row = col = c = 0; c < str.length; c++) {
147 | var cc = str[c], nc = str[c+1]; // current character, next character
148 | if (arr.length <= row) { arr.push([]); }
149 | if (arr[row].length <= col) { arr[row].push([""]); }
150 |
151 | // If the current character is a quotation mark, and we're inside a
152 | // quoted field, and the next character is also a quotation mark,
153 | // add a quotation mark to the current column and skip the next character
154 | if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
155 |
156 | // If it's just one quotation mark, begin/end quoted field
157 | if (cc == '"') { quote = !quote; continue; }
158 |
159 | // If it's a comma and we're not in a quoted field, move on to the next column
160 | if (cc == ',' && !quote) { ++col; continue; }
161 |
162 | // If it's a newline and we're not in a quoted field, move on to the next
163 | // row and move to column 0 of that new row
164 | if (cc == '\n' && !quote) { ++row; col = 0; continue; }
165 |
166 | // Otherwise, append the current character to the current column
167 | arr[row][col] += cc;
168 | }
169 | return arr;
170 | }
171 |
172 | function getCSVValue(csvArray, key) {
173 | if (csvArray.length > 1) {
174 | var index = csvArray[0].indexOf(key);
175 | if (index == -1) {
176 | return undefined;
177 | }
178 | return csvArray[1][index].trim();
179 | }
180 | return undefined;
181 | }
182 |
183 |
184 | /* Heaters */
185 |
186 | function setHeatersInUse() {
187 | heatersInUse = [];
188 | for(var heater = 0; heater < maxHeaters; heater++) {
189 | var heaterAssigned = (heater == bedHeater);
190 | heaterAssigned |= (heater == chamberHeater);
191 | heaterAssigned |= (heater == cabinetHeater);
192 | heaterAssigned |= (getToolsByHeater(heater).length > 0);
193 | heatersInUse.push(heaterAssigned);
194 | }
195 | }
196 |
197 |
198 | /* Tool Mapping */
199 |
200 | var initialTools = "";
201 | $("#table_tools > tbody > tr[data-tool]").each(function() {
202 | initialTools += this.outerHTML;
203 | });
204 |
205 | function setToolMapping(mapping) {
206 | var mappingHasChanged = (toolMapping == undefined);
207 | if (!mappingHasChanged) {
208 | // Add missing fields in case we're talking with an older firmware version
209 | for(var i = 0; i < mapping.length; i++) {
210 | if (!mapping[i].hasOwnProperty("number")) {
211 | // This is rather ugly and should be never used
212 | mapping[i].number = i + 1;
213 | }
214 |
215 | if (!mapping[i].hasOwnProperty("name")) {
216 | mapping[i].name = "";
217 | }
218 | }
219 |
220 | // Compare the old tool mapping with the new one
221 | if (mapping.length != toolMapping.length) {
222 | mappingHasChanged = true;
223 | } else {
224 | for(var i = 0; i < mapping.length; i++) {
225 | // Stop immediately if anything significant has changed
226 | if (mapping[i].number != toolMapping[i].number ||
227 | mapping[i].name != toolMapping[i].name ||
228 | mapping[i].hasOwnProperty("filament") != toolMapping[i].hasOwnProperty("filament") ||
229 | mapping[i].heaters.toString() != toolMapping[i].heaters.toString() ||
230 | mapping[i].drives.toString() != toolMapping[i].drives.toString()
231 | ) {
232 | mappingHasChanged = true;
233 | break;
234 | }
235 |
236 | // If only the filament has changed, update it
237 | if (mapping[i].hasOwnProperty("filament") && mapping[i].filament != toolMapping[i].filament) {
238 | setToolFilament(i, mapping[i].filament);
239 | }
240 | }
241 | }
242 | }
243 |
244 | // See if anything significant has changed
245 | if (mappingHasChanged) {
246 | toolMapping = mapping;
247 | setHeatersInUse();
248 |
249 | // TODO: The web interface has no idea which drive is assigned to which axis/extruder,
250 | // so the following cannot be fully implemented yet.
251 |
252 | /**
253 | // Find out which drives may be assigned to XYZ
254 | xyzAxisMapping = [[0], [1], [2]];
255 | if (toolMapping != undefined) {
256 | for(var i = 0; i < toolMapping.length; i++) {
257 | var tool = toolMapping[i];
258 | if (tool.hasOwnProperty("axisMap")) {
259 | for(var k = 0; k < tool.axisMap.length; i++) {
260 | for(var l = 0; l < tool.axisMap[k].length; l++) {
261 | var mappedDrive = tool.axisMap[k][l];
262 | if (xyzAxisMapping[k].indexOf(mappedDrive) == -1) {
263 | xyzAxisMapping[k].push(mappedDrive);
264 | }
265 | }
266 | }
267 | }
268 | }
269 | }
270 |
271 | // Adjust the column headers of the XYZ position to match this mapping
272 | if (axisMapping[0].length > 1) {
273 | var content = "X (";
274 |
275 |
276 | content += ")";
277 | } else {
278 | $("#th_x").text("X");
279 | }*/
280 |
281 | return true;
282 | }
283 | return false;
284 | }
285 |
286 | function setToolFilament(tool, filament) {
287 | toolMapping[tool].filament = filament;
288 |
289 | var label = "T" + toolMapping[tool].number + ((filament == "") ? "" : (" - " + filament));
290 | $("#table_tools tr[data-tool='" + toolMapping[tool].number + "'] > th:first-child > span.text-muted").text(label);
291 |
292 | var filamentLabel = (filament == "") ? T("none") : filament;
293 | $("#page_tools div[data-tool='" + toolMapping[tool].number + "'] dd.filament").text(filamentLabel);
294 | }
295 |
296 | function updateFixedToolTemps(rows) {
297 | settings.defaultActiveTemps.forEach(function(temp) {
298 | rows.find(".ul-active-temp").append('