diff --git a/README.md b/README.md
index 76953a0..e816400 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,11 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
If you would like to include this code in a project which is not licensed under the AGPL V3, please contact the author at andrewhodel@gmail.com
+## additional credits
+This repo is a mixture of the above repo, and work done by bryanchanrobot
+https://github.com/wgbartley/grblweb
+https://github.com/brychanrobot/grblweb
+
## Raspberry Pi prebuilt Image
There is a prebuilt Raspberry Pi Image that is fully configured. More information and a link to the .img can be found at http://xyzbots.com
@@ -35,6 +40,12 @@ https://github.com/grbl/grbl/wiki/Configuring-Grbl-v0.8
http://onehossshay.wordpress.com/2011/08/21/grbl-how-it-works-and-other-thoughts/
## Installation
+node --version
+v0.10.32
+npm --version
+1.4.28
+
+I ran into errors with a base install of nodejs on ubuntu
```
git clone https://github.com/andrewhodel/grblweb.git
@@ -58,6 +69,9 @@ node server.js
npm install -g forever
forever start server.js
```
+## Camera
+you'll presently need to install mjpg-streamer, working on something more integrated, but no promises
+https://github.com/jacksonliam/mjpg-streamer
## Access
@@ -72,3 +86,13 @@ Read http://www.hobbytronics.co.uk/raspberry-pi-serial-port
Set config.usettyAMA0 to 1 in config.js
This is already enabled on the prebuilt GRBLWeb Raspbian image.
+
+## Fixes
+script is looking for mjpg port again
+camera viewport readded to main screen
+minjs stuff is included again (have to check to see if that's actually "ok" or if I need to point to downloads there as well)
+USB serial port is available for selection
+
+## dev setup
+raspi V1 - raspi camera - linksprite 3 axis CNC
+http://linksprite.com/wiki/index.php5?title=DIY_CNC_3_Axis_Engraver_Machine_PCB_Milling_Wood_Carving_Router_Kit_Arduino_Grbl
diff --git a/config.js b/config.js
index 05922db..6954357 100644
--- a/config.js
+++ b/config.js
@@ -1,12 +1,57 @@
+// !!! All values that are distance measures should be entered in millimeters !!!
var config = {};
-config.webPort = 8000;
+// Port to listen on for web site
+config.webPort = 80;
-config.serialBaudRate = 9600;
+// Serial baud rate to Arduino
+config.serialBaudRate = 115200;
+// Use /dev/ttyAMA0
config.usettyAMA0 = 0;
+// Enable simple jog controls
+config.enableSimpleControls = 1;
+
+// Enable probe controls
+config.enableProbeControls = 1;
+
+// Enable jsCut
+config.enableJsCut = true;
+
+// Default step increment for jogging
+config.jogControlDefaultIncr = 0.1;
+
+// Default feed rate for jogging
+config.jogControlDefaultFeed = 1000;
+
+// X offset for probe
+config.probeControlXOffset = -0.5;
+
+// Y offset for probe
+config.probeControlYOffset = -1.5;
+
+// Z offset for probe
+config.probeControlZOffset = 3.1;
+
+// Maximum you can move Z with any 1 command
+config.maxMoveZ = 50;
+
+// Auto-read temperature
+config.enablePiTemperature = false;
+config.piTemperatureFahrenheit = true;
+config.piTemperatureFile = [
+ '/sys/class/thermal/thermal_zone0/temp',
+ '/sys/bus/w1/devices/28-000002aa87dd/w1_slave'
+];
+
+config.showUnitsBackdrop = false;
+
+// TO DO:
+// - Add probe diameters
+// - Allow configuration of gcode for probing buttons
+
// expects a webcam stream from mjpg_streamer
config.webcamPort = 8080;
diff --git a/i/index.html b/i/index.html
index 5f0aa64..6040200 100644
--- a/i/index.html
+++ b/i/index.html
@@ -1,3 +1,9 @@
+
+
-
+
+
+
+
+
diff --git a/i/jquery.powertip/jquery.powertip.js b/i/jquery.powertip/jquery.powertip.js
new file mode 100644
index 0000000..b91552d
--- /dev/null
+++ b/i/jquery.powertip/jquery.powertip.js
@@ -0,0 +1,1355 @@
+/*!
+ PowerTip v1.3.0 (2017-01-15)
+ https://stevenbenner.github.io/jquery-powertip/
+ Copyright (c) 2017 Steven Benner (http://stevenbenner.com/).
+ Released under MIT license.
+ https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt
+*/
+(function(root, factory) {
+ // support loading the plugin via common patterns
+ if (typeof define === 'function' && define.amd) {
+ // load the plugin as an amd module
+ define([ 'jquery' ], factory);
+ } else if (typeof module === 'object' && module.exports) {
+ // load the plugin as a commonjs module
+ module.exports = factory(require('jquery'));
+ } else {
+ // load the plugin as a global
+ factory(root.jQuery);
+ }
+}(this, function($) {
+
+ // useful private variables
+ var $document = $(document),
+ $window = $(window),
+ $body = $('body');
+
+ // constants
+ var DATA_DISPLAYCONTROLLER = 'displayController',
+ DATA_HASACTIVEHOVER = 'hasActiveHover',
+ DATA_FORCEDOPEN = 'forcedOpen',
+ DATA_HASMOUSEMOVE = 'hasMouseMove',
+ DATA_MOUSEONTOTIP = 'mouseOnToPopup',
+ DATA_ORIGINALTITLE = 'originalTitle',
+ DATA_POWERTIP = 'powertip',
+ DATA_POWERTIPJQ = 'powertipjq',
+ DATA_POWERTIPTARGET = 'powertiptarget',
+ EVENT_NAMESPACE = '.powertip',
+ RAD2DEG = 180 / Math.PI;
+
+ /**
+ * Session data
+ * Private properties global to all powerTip instances
+ */
+ var session = {
+ elements: null,
+ tooltips: null,
+ isTipOpen: false,
+ isFixedTipOpen: false,
+ isClosing: false,
+ tipOpenImminent: false,
+ activeHover: null,
+ currentX: 0,
+ currentY: 0,
+ previousX: 0,
+ previousY: 0,
+ desyncTimeout: null,
+ closeDelayTimeout: null,
+ mouseTrackingActive: false,
+ delayInProgress: false,
+ windowWidth: 0,
+ windowHeight: 0,
+ scrollTop: 0,
+ scrollLeft: 0
+ };
+
+ /**
+ * Collision enumeration
+ * @enum {number}
+ */
+ var Collision = {
+ none: 0,
+ top: 1,
+ bottom: 2,
+ left: 4,
+ right: 8
+ };
+
+ /**
+ * Display hover tooltips on the matched elements.
+ * @param {(Object|string)=} opts The options object to use for the plugin, or
+ * the name of a method to invoke on the first matched element.
+ * @param {*=} [arg] Argument for an invoked method (optional).
+ * @return {jQuery} jQuery object for the matched selectors.
+ */
+ $.fn.powerTip = function(opts, arg) {
+ var targetElements = this,
+ options,
+ tipController;
+
+ // don't do any work if there were no matched elements
+ if (!targetElements.length) {
+ return targetElements;
+ }
+
+ // handle api method calls on the plugin, e.g. powerTip('hide')
+ if ($.type(opts) === 'string' && $.powerTip[opts]) {
+ return $.powerTip[opts].call(targetElements, targetElements, arg);
+ }
+
+ // extend options and instantiate TooltipController
+ options = $.extend({}, $.fn.powerTip.defaults, opts);
+ tipController = new TooltipController(options);
+
+ // hook mouse and viewport dimension tracking
+ initTracking();
+
+ // setup the elements
+ targetElements.each(function elementSetup() {
+ var $this = $(this),
+ dataPowertip = $this.data(DATA_POWERTIP),
+ dataElem = $this.data(DATA_POWERTIPJQ),
+ dataTarget = $this.data(DATA_POWERTIPTARGET),
+ title;
+
+ // handle repeated powerTip calls on the same element by destroying the
+ // original instance hooked to it and replacing it with this call
+ if ($this.data(DATA_DISPLAYCONTROLLER)) {
+ $.powerTip.destroy($this);
+ }
+
+ // attempt to use title attribute text if there is no data-powertip,
+ // data-powertipjq or data-powertiptarget. If we do use the title
+ // attribute, delete the attribute so the browser will not show it
+ title = $this.attr('title');
+ if (!dataPowertip && !dataTarget && !dataElem && title) {
+ $this.data(DATA_POWERTIP, title);
+ $this.data(DATA_ORIGINALTITLE, title);
+ $this.removeAttr('title');
+ }
+
+ // create hover controllers for each element
+ $this.data(
+ DATA_DISPLAYCONTROLLER,
+ new DisplayController($this, options, tipController)
+ );
+ });
+
+ // attach events to matched elements if the manual option is not enabled
+ if (!options.manual) {
+ // attach open events
+ $.each(options.openEvents, function(idx, evt) {
+ if ($.inArray(evt, options.closeEvents) > -1) {
+ // event is in both openEvents and closeEvents, so toggle it
+ targetElements.on(evt + EVENT_NAMESPACE, function elementToggle(event) {
+ $.powerTip.toggle(this, event);
+ });
+ } else {
+ targetElements.on(evt + EVENT_NAMESPACE, function elementOpen(event) {
+ $.powerTip.show(this, event);
+ });
+ }
+ });
+
+ // attach close events
+ $.each(options.closeEvents, function(idx, evt) {
+ if ($.inArray(evt, options.openEvents) < 0) {
+ targetElements.on(evt + EVENT_NAMESPACE, function elementClose(event) {
+ // set immediate to true for any event without mouse info
+ $.powerTip.hide(this, !isMouseEvent(event));
+ });
+ }
+ });
+
+ // attach escape key close event
+ targetElements.on('keydown' + EVENT_NAMESPACE, function elementKeyDown(event) {
+ // always close tooltip when the escape key is pressed
+ if (event.keyCode === 27) {
+ $.powerTip.hide(this, true);
+ }
+ });
+ }
+
+ // remember elements that the plugin is attached to
+ session.elements = session.elements ? session.elements.add(targetElements) : targetElements;
+
+ return targetElements;
+ };
+
+ /**
+ * Default options for the powerTip plugin.
+ */
+ $.fn.powerTip.defaults = {
+ fadeInTime: 200,
+ fadeOutTime: 100,
+ followMouse: false,
+ popupId: 'powerTip',
+ popupClass: null,
+ intentSensitivity: 7,
+ intentPollInterval: 100,
+ closeDelay: 100,
+ placement: 'n',
+ smartPlacement: false,
+ offset: 10,
+ mouseOnToPopup: false,
+ manual: false,
+ openEvents: [ 'mouseenter', 'focus' ],
+ closeEvents: [ 'mouseleave', 'blur' ]
+ };
+
+ /**
+ * Default smart placement priority lists.
+ * The first item in the array is the highest priority, the last is the lowest.
+ * The last item is also the default, which will be used if all previous options
+ * do not fit.
+ */
+ $.fn.powerTip.smartPlacementLists = {
+ n: [ 'n', 'ne', 'nw', 's' ],
+ e: [ 'e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e' ],
+ s: [ 's', 'se', 'sw', 'n' ],
+ w: [ 'w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w' ],
+ nw: [ 'nw', 'w', 'sw', 'n', 's', 'se', 'nw' ],
+ ne: [ 'ne', 'e', 'se', 'n', 's', 'sw', 'ne' ],
+ sw: [ 'sw', 'w', 'nw', 's', 'n', 'ne', 'sw' ],
+ se: [ 'se', 'e', 'ne', 's', 'n', 'nw', 'se' ],
+ 'nw-alt': [ 'nw-alt', 'n', 'ne-alt', 'sw-alt', 's', 'se-alt', 'w', 'e' ],
+ 'ne-alt': [ 'ne-alt', 'n', 'nw-alt', 'se-alt', 's', 'sw-alt', 'e', 'w' ],
+ 'sw-alt': [ 'sw-alt', 's', 'se-alt', 'nw-alt', 'n', 'ne-alt', 'w', 'e' ],
+ 'se-alt': [ 'se-alt', 's', 'sw-alt', 'ne-alt', 'n', 'nw-alt', 'e', 'w' ]
+ };
+
+ /**
+ * Public API
+ */
+ $.powerTip = {
+ /**
+ * Attempts to show the tooltip for the specified element.
+ * @param {jQuery|Element} element The element to open the tooltip for.
+ * @param {jQuery.Event=} event jQuery event for hover intent and mouse
+ * tracking (optional).
+ * @return {jQuery|Element} The original jQuery object or DOM Element.
+ */
+ show: function apiShowTip(element, event) {
+ // if we were given a mouse event then run the hover intent testing,
+ // otherwise, simply show the tooltip asap
+ if (isMouseEvent(event)) {
+ trackMouse(event);
+ session.previousX = event.pageX;
+ session.previousY = event.pageY;
+ $(element).data(DATA_DISPLAYCONTROLLER).show();
+ } else {
+ $(element).first().data(DATA_DISPLAYCONTROLLER).show(true, true);
+ }
+ return element;
+ },
+
+ /**
+ * Repositions the tooltip on the element.
+ * @param {jQuery|Element} element The element the tooltip is shown for.
+ * @return {jQuery|Element} The original jQuery object or DOM Element.
+ */
+ reposition: function apiResetPosition(element) {
+ $(element).first().data(DATA_DISPLAYCONTROLLER).resetPosition();
+ return element;
+ },
+
+ /**
+ * Attempts to close any open tooltips.
+ * @param {(jQuery|Element)=} element The element with the tooltip that
+ * should be closed (optional).
+ * @param {boolean=} immediate Disable close delay (optional).
+ * @return {jQuery|Element|undefined} The original jQuery object or DOM
+ * Element, if one was specified.
+ */
+ hide: function apiCloseTip(element, immediate) {
+ var displayController;
+
+ // set immediate to true when no element is specified
+ immediate = element ? immediate : true;
+
+ // find the relevant display controller
+ if (element) {
+ displayController = $(element).first().data(DATA_DISPLAYCONTROLLER);
+ } else if (session.activeHover) {
+ displayController = session.activeHover.data(DATA_DISPLAYCONTROLLER);
+ }
+
+ // if found, hide the tip
+ if (displayController) {
+ displayController.hide(immediate);
+ }
+
+ return element;
+ },
+
+ /**
+ * Toggles the tooltip for the specified element. This will open a closed
+ * tooltip, or close an open tooltip.
+ * @param {jQuery|Element} element The element with the tooltip that
+ * should be toggled.
+ * @param {jQuery.Event=} event jQuery event for hover intent and mouse
+ * tracking (optional).
+ * @return {jQuery|Element} The original jQuery object or DOM Element.
+ */
+ toggle: function apiToggle(element, event) {
+ if (session.activeHover && session.activeHover.is(element)) {
+ // tooltip for element is active, so close it
+ $.powerTip.hide(element, !isMouseEvent(event));
+ } else {
+ // tooltip for element is not active, so open it
+ $.powerTip.show(element, event);
+ }
+ return element;
+ },
+
+ /**
+ * Destroy and roll back any powerTip() instance on the specified elements.
+ * If no elements are specified then all elements that the plugin is
+ * currently attached to will be rolled back.
+ * @param {(jQuery|Element)=} element The element with the powerTip instance.
+ * @return {jQuery|Element|undefined} The original jQuery object or DOM
+ * Element, if one was specified.
+ */
+ destroy: function apiDestroy(element) {
+ var $element = element ? $(element) : session.elements;
+
+ // if the plugin is not hooked to any elements then there is no point
+ // trying to destroy anything, or dealing with the possible errors
+ if (!session.elements || session.elements.length === 0) {
+ return element;
+ }
+
+ // unhook events and destroy plugin changes to each element
+ $element.off(EVENT_NAMESPACE).each(function destroy() {
+ var $this = $(this),
+ dataAttributes = [
+ DATA_ORIGINALTITLE,
+ DATA_DISPLAYCONTROLLER,
+ DATA_HASACTIVEHOVER,
+ DATA_FORCEDOPEN
+ ];
+
+ // revert title attribute
+ if ($this.data(DATA_ORIGINALTITLE)) {
+ $this.attr('title', $this.data(DATA_ORIGINALTITLE));
+ dataAttributes.push(DATA_POWERTIP);
+ }
+
+ // remove data attributes
+ $this.removeData(dataAttributes);
+ });
+
+ // remove destroyed element from active elements collection
+ session.elements = session.elements.not($element);
+
+ // if there are no active elements left then we will unhook all of the
+ // events that we've bound code to and remove the tooltip elements
+ if (session.elements.length === 0) {
+ $window.off(EVENT_NAMESPACE);
+ $document.off(EVENT_NAMESPACE);
+ session.mouseTrackingActive = false;
+ session.tooltips.remove();
+ session.tooltips = null;
+ }
+
+ return element;
+ }
+ };
+
+ // API aliasing
+ $.powerTip.showTip = $.powerTip.show;
+ $.powerTip.closeTip = $.powerTip.hide;
+
+ /**
+ * Creates a new CSSCoordinates object.
+ * @private
+ * @constructor
+ */
+ function CSSCoordinates() {
+ var me = this;
+
+ // initialize object properties
+ me.top = 'auto';
+ me.left = 'auto';
+ me.right = 'auto';
+ me.bottom = 'auto';
+
+ /**
+ * Set a property to a value.
+ * @private
+ * @param {string} property The name of the property.
+ * @param {number} value The value of the property.
+ */
+ me.set = function(property, value) {
+ if ($.isNumeric(value)) {
+ me[property] = Math.round(value);
+ }
+ };
+ }
+
+ /**
+ * Creates a new tooltip display controller.
+ * @private
+ * @constructor
+ * @param {jQuery} element The element that this controller will handle.
+ * @param {Object} options Options object containing settings.
+ * @param {TooltipController} tipController The TooltipController object for
+ * this instance.
+ */
+ function DisplayController(element, options, tipController) {
+ var hoverTimer = null,
+ myCloseDelay = null;
+
+ /**
+ * Begins the process of showing a tooltip.
+ * @private
+ * @param {boolean=} immediate Skip intent testing (optional).
+ * @param {boolean=} forceOpen Ignore cursor position and force tooltip to
+ * open (optional).
+ */
+ function openTooltip(immediate, forceOpen) {
+ cancelTimer();
+ if (!element.data(DATA_HASACTIVEHOVER)) {
+ if (!immediate) {
+ session.tipOpenImminent = true;
+ hoverTimer = setTimeout(
+ function intentDelay() {
+ hoverTimer = null;
+ checkForIntent();
+ },
+ options.intentPollInterval
+ );
+ } else {
+ if (forceOpen) {
+ element.data(DATA_FORCEDOPEN, true);
+ }
+ closeAnyDelayed();
+ tipController.showTip(element);
+ }
+ } else {
+ // cursor left and returned to this element, cancel close
+ cancelClose();
+ }
+ }
+
+ /**
+ * Begins the process of closing a tooltip.
+ * @private
+ * @param {boolean=} disableDelay Disable close delay (optional).
+ */
+ function closeTooltip(disableDelay) {
+ // if this instance already has a close delay in progress then halt it
+ if (myCloseDelay) {
+ myCloseDelay = session.closeDelayTimeout = clearTimeout(myCloseDelay);
+ session.delayInProgress = false;
+ }
+ cancelTimer();
+ session.tipOpenImminent = false;
+ if (element.data(DATA_HASACTIVEHOVER)) {
+ element.data(DATA_FORCEDOPEN, false);
+ if (!disableDelay) {
+ session.delayInProgress = true;
+ session.closeDelayTimeout = setTimeout(
+ function closeDelay() {
+ session.closeDelayTimeout = null;
+ tipController.hideTip(element);
+ session.delayInProgress = false;
+ myCloseDelay = null;
+ },
+ options.closeDelay
+ );
+ // save internal reference close delay id so we can check if the
+ // active close delay belongs to this instance
+ myCloseDelay = session.closeDelayTimeout;
+ } else {
+ tipController.hideTip(element);
+ }
+ }
+ }
+
+ /**
+ * Checks mouse position to make sure that the user intended to hover on the
+ * specified element before showing the tooltip.
+ * @private
+ */
+ function checkForIntent() {
+ // calculate mouse position difference
+ var xDifference = Math.abs(session.previousX - session.currentX),
+ yDifference = Math.abs(session.previousY - session.currentY),
+ totalDifference = xDifference + yDifference;
+
+ // check if difference has passed the sensitivity threshold
+ if (totalDifference < options.intentSensitivity) {
+ cancelClose();
+ closeAnyDelayed();
+ tipController.showTip(element);
+ } else {
+ // try again
+ session.previousX = session.currentX;
+ session.previousY = session.currentY;
+ openTooltip();
+ }
+ }
+
+ /**
+ * Cancels active hover timer.
+ * @private
+ * @param {boolean=} stopClose Cancel any active close delay timer.
+ */
+ function cancelTimer(stopClose) {
+ hoverTimer = clearTimeout(hoverTimer);
+ // cancel the current close delay if the active close delay is for this
+ // element or the stopClose argument is true
+ if (session.closeDelayTimeout && myCloseDelay === session.closeDelayTimeout || stopClose) {
+ cancelClose();
+ }
+ }
+
+ /**
+ * Cancels any active close delay timer.
+ * @private
+ */
+ function cancelClose() {
+ session.closeDelayTimeout = clearTimeout(session.closeDelayTimeout);
+ session.delayInProgress = false;
+ }
+
+ /**
+ * Asks any tooltips waiting on their close delay to close now.
+ * @private
+ */
+ function closeAnyDelayed() {
+ // if another element is waiting for its close delay then we should ask
+ // it to close immediately so we can proceed without unexpected timeout
+ // code being run during this tooltip's lifecycle
+ if (session.delayInProgress && session.activeHover && !session.activeHover.is(element)) {
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).hide(true);
+ }
+ }
+
+ /**
+ * Repositions the tooltip on this element.
+ * @private
+ */
+ function repositionTooltip() {
+ tipController.resetPosition(element);
+ }
+
+ // expose the methods
+ this.show = openTooltip;
+ this.hide = closeTooltip;
+ this.cancel = cancelTimer;
+ this.resetPosition = repositionTooltip;
+ }
+
+ /**
+ * Creates a new Placement Calculator.
+ * @private
+ * @constructor
+ */
+ function PlacementCalculator() {
+ /**
+ * Compute the CSS position to display a tooltip at the specified placement
+ * relative to the specified element.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ * @param {string} placement The placement for the tooltip.
+ * @param {number} tipWidth Width of the tooltip element in pixels.
+ * @param {number} tipHeight Height of the tooltip element in pixels.
+ * @param {number} offset Distance to offset tooltips in pixels.
+ * @return {CSSCoordinates} A CSSCoordinates object with the position.
+ */
+ function computePlacementCoords(element, placement, tipWidth, tipHeight, offset) {
+ var placementBase = placement.split('-')[0], // ignore 'alt' for corners
+ coords = new CSSCoordinates(),
+ position;
+
+ if (isSvgElement(element)) {
+ position = getSvgPlacement(element, placementBase);
+ } else {
+ position = getHtmlPlacement(element, placementBase);
+ }
+
+ // calculate the appropriate x and y position in the document
+ switch (placement) {
+ case 'n':
+ coords.set('left', position.left - (tipWidth / 2));
+ coords.set('bottom', session.windowHeight - position.top + offset);
+ break;
+ case 'e':
+ coords.set('left', position.left + offset);
+ coords.set('top', position.top - (tipHeight / 2));
+ break;
+ case 's':
+ coords.set('left', position.left - (tipWidth / 2));
+ coords.set('top', position.top + offset);
+ break;
+ case 'w':
+ coords.set('top', position.top - (tipHeight / 2));
+ coords.set('right', session.windowWidth - position.left + offset);
+ break;
+ case 'nw':
+ coords.set('bottom', session.windowHeight - position.top + offset);
+ coords.set('right', session.windowWidth - position.left - 20);
+ break;
+ case 'nw-alt':
+ coords.set('left', position.left);
+ coords.set('bottom', session.windowHeight - position.top + offset);
+ break;
+ case 'ne':
+ coords.set('left', position.left - 20);
+ coords.set('bottom', session.windowHeight - position.top + offset);
+ break;
+ case 'ne-alt':
+ coords.set('bottom', session.windowHeight - position.top + offset);
+ coords.set('right', session.windowWidth - position.left);
+ break;
+ case 'sw':
+ coords.set('top', position.top + offset);
+ coords.set('right', session.windowWidth - position.left - 20);
+ break;
+ case 'sw-alt':
+ coords.set('left', position.left);
+ coords.set('top', position.top + offset);
+ break;
+ case 'se':
+ coords.set('left', position.left - 20);
+ coords.set('top', position.top + offset);
+ break;
+ case 'se-alt':
+ coords.set('top', position.top + offset);
+ coords.set('right', session.windowWidth - position.left);
+ break;
+ }
+
+ return coords;
+ }
+
+ /**
+ * Finds the tooltip attachment point in the document for a HTML DOM element
+ * for the specified placement.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ * @param {string} placement The placement for the tooltip.
+ * @return {Object} An object with the top,left position values.
+ */
+ function getHtmlPlacement(element, placement) {
+ var objectOffset = element.offset(),
+ objectWidth = element.outerWidth(),
+ objectHeight = element.outerHeight(),
+ left,
+ top;
+
+ // calculate the appropriate x and y position in the document
+ switch (placement) {
+ case 'n':
+ left = objectOffset.left + objectWidth / 2;
+ top = objectOffset.top;
+ break;
+ case 'e':
+ left = objectOffset.left + objectWidth;
+ top = objectOffset.top + objectHeight / 2;
+ break;
+ case 's':
+ left = objectOffset.left + objectWidth / 2;
+ top = objectOffset.top + objectHeight;
+ break;
+ case 'w':
+ left = objectOffset.left;
+ top = objectOffset.top + objectHeight / 2;
+ break;
+ case 'nw':
+ left = objectOffset.left;
+ top = objectOffset.top;
+ break;
+ case 'ne':
+ left = objectOffset.left + objectWidth;
+ top = objectOffset.top;
+ break;
+ case 'sw':
+ left = objectOffset.left;
+ top = objectOffset.top + objectHeight;
+ break;
+ case 'se':
+ left = objectOffset.left + objectWidth;
+ top = objectOffset.top + objectHeight;
+ break;
+ }
+
+ return {
+ top: top,
+ left: left
+ };
+ }
+
+ /**
+ * Finds the tooltip attachment point in the document for a SVG element for
+ * the specified placement.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ * @param {string} placement The placement for the tooltip.
+ * @return {Object} An object with the top,left position values.
+ */
+ function getSvgPlacement(element, placement) {
+ var svgElement = element.closest('svg')[0],
+ domElement = element[0],
+ point = svgElement.createSVGPoint(),
+ boundingBox = domElement.getBBox(),
+ matrix = domElement.getScreenCTM(),
+ halfWidth = boundingBox.width / 2,
+ halfHeight = boundingBox.height / 2,
+ placements = [],
+ placementKeys = [ 'nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w' ],
+ coords,
+ rotation,
+ steps,
+ x;
+
+ function pushPlacement() {
+ placements.push(point.matrixTransform(matrix));
+ }
+
+ // get bounding box corners and midpoints
+ point.x = boundingBox.x;
+ point.y = boundingBox.y;
+ pushPlacement();
+ point.x += halfWidth;
+ pushPlacement();
+ point.x += halfWidth;
+ pushPlacement();
+ point.y += halfHeight;
+ pushPlacement();
+ point.y += halfHeight;
+ pushPlacement();
+ point.x -= halfWidth;
+ pushPlacement();
+ point.x -= halfWidth;
+ pushPlacement();
+ point.y -= halfHeight;
+ pushPlacement();
+
+ // determine rotation
+ if (placements[0].y !== placements[1].y || placements[0].x !== placements[7].x) {
+ rotation = Math.atan2(matrix.b, matrix.a) * RAD2DEG;
+ steps = Math.ceil(((rotation % 360) - 22.5) / 45);
+ if (steps < 1) {
+ steps += 8;
+ }
+ while (steps--) {
+ placementKeys.push(placementKeys.shift());
+ }
+ }
+
+ // find placement
+ for (x = 0; x < placements.length; x++) {
+ if (placementKeys[x] === placement) {
+ coords = placements[x];
+ break;
+ }
+ }
+
+ return {
+ top: coords.y + session.scrollTop,
+ left: coords.x + session.scrollLeft
+ };
+ }
+
+ // expose methods
+ this.compute = computePlacementCoords;
+ }
+
+ /**
+ * Creates a new tooltip controller.
+ * @private
+ * @constructor
+ * @param {Object} options Options object containing settings.
+ */
+ function TooltipController(options) {
+ var placementCalculator = new PlacementCalculator(),
+ tipElement = $('#' + options.popupId);
+
+ // build and append tooltip div if it does not already exist
+ if (tipElement.length === 0) {
+ tipElement = $('', { id: options.popupId });
+ // grab body element if it was not populated when the script loaded
+ // note: this hack exists solely for jsfiddle support
+ if ($body.length === 0) {
+ $body = $('body');
+ }
+ $body.append(tipElement);
+ // remember the tooltip elements that the plugin has created
+ session.tooltips = session.tooltips ? session.tooltips.add(tipElement) : tipElement;
+ }
+
+ // hook mousemove for cursor follow tooltips
+ if (options.followMouse) {
+ // only one positionTipOnCursor hook per tooltip element, please
+ if (!tipElement.data(DATA_HASMOUSEMOVE)) {
+ $document.on('mousemove' + EVENT_NAMESPACE, positionTipOnCursor);
+ $window.on('scroll' + EVENT_NAMESPACE, positionTipOnCursor);
+ tipElement.data(DATA_HASMOUSEMOVE, true);
+ }
+ }
+
+ /**
+ * Gives the specified element the active-hover state and queues up the
+ * showTip function.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ */
+ function beginShowTip(element) {
+ element.data(DATA_HASACTIVEHOVER, true);
+ // show tooltip, asap
+ tipElement.queue(function queueTipInit(next) {
+ showTip(element);
+ next();
+ });
+ }
+
+ /**
+ * Shows the tooltip, as soon as possible.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ */
+ function showTip(element) {
+ var tipContent;
+
+ // it is possible, especially with keyboard navigation, to move on to
+ // another element with a tooltip during the queue to get to this point
+ // in the code. if that happens then we need to not proceed or we may
+ // have the fadeout callback for the last tooltip execute immediately
+ // after this code runs, causing bugs.
+ if (!element.data(DATA_HASACTIVEHOVER)) {
+ return;
+ }
+
+ // if the tooltip is open and we got asked to open another one then the
+ // old one is still in its fadeOut cycle, so wait and try again
+ if (session.isTipOpen) {
+ if (!session.isClosing) {
+ hideTip(session.activeHover);
+ }
+ tipElement.delay(100).queue(function queueTipAgain(next) {
+ showTip(element);
+ next();
+ });
+ return;
+ }
+
+ // trigger powerTipPreRender event
+ element.trigger('powerTipPreRender');
+
+ // set tooltip content
+ tipContent = getTooltipContent(element);
+ if (tipContent) {
+ tipElement.empty().append(tipContent);
+ } else {
+ // we have no content to display, give up
+ return;
+ }
+
+ // trigger powerTipRender event
+ element.trigger('powerTipRender');
+
+ session.activeHover = element;
+ session.isTipOpen = true;
+
+ tipElement.data(DATA_MOUSEONTOTIP, options.mouseOnToPopup);
+
+ // set tooltip position
+ if (!options.followMouse) {
+ positionTipOnElement(element);
+ session.isFixedTipOpen = true;
+ } else {
+ positionTipOnCursor();
+ }
+
+ // add custom class to tooltip element
+ tipElement.addClass(options.popupClass);
+
+ // close tooltip when clicking anywhere on the page, with the exception
+ // of the tooltip's trigger element and any elements that are within a
+ // tooltip that has 'mouseOnToPopup' option enabled
+ if (!element.data(DATA_FORCEDOPEN)) {
+ $document.on('click' + EVENT_NAMESPACE, function documentClick(event) {
+ var target = event.target;
+ if (target !== element[0]) {
+ if (options.mouseOnToPopup) {
+ if (target !== tipElement[0] && !$.contains(tipElement[0], target)) {
+ $.powerTip.hide();
+ }
+ } else {
+ $.powerTip.hide();
+ }
+ }
+ });
+ }
+
+ // if we want to be able to mouse on to the tooltip then we need to
+ // attach hover events to the tooltip that will cancel a close request
+ // on mouseenter and start a new close request on mouseleave
+ // only hook these listeners if we're not in manual mode
+ if (options.mouseOnToPopup && !options.manual) {
+ tipElement.on('mouseenter' + EVENT_NAMESPACE, function tipMouseEnter() {
+ // check activeHover in case the mouse cursor entered the
+ // tooltip during the fadeOut and close cycle
+ if (session.activeHover) {
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).cancel();
+ }
+ });
+ tipElement.on('mouseleave' + EVENT_NAMESPACE, function tipMouseLeave() {
+ // check activeHover in case the mouse cursor left the tooltip
+ // during the fadeOut and close cycle
+ if (session.activeHover) {
+ session.activeHover.data(DATA_DISPLAYCONTROLLER).hide();
+ }
+ });
+ }
+
+ // fadein
+ tipElement.fadeIn(options.fadeInTime, function fadeInCallback() {
+ // start desync polling
+ if (!session.desyncTimeout) {
+ session.desyncTimeout = setInterval(closeDesyncedTip, 500);
+ }
+
+ // trigger powerTipOpen event
+ element.trigger('powerTipOpen');
+ });
+ }
+
+ /**
+ * Hides the tooltip.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ */
+ function hideTip(element) {
+ // reset session
+ session.isClosing = true;
+ session.isTipOpen = false;
+
+ // stop desync polling
+ session.desyncTimeout = clearInterval(session.desyncTimeout);
+
+ // reset element state
+ element.data(DATA_HASACTIVEHOVER, false);
+ element.data(DATA_FORCEDOPEN, false);
+
+ // remove document click handler
+ $document.off('click' + EVENT_NAMESPACE);
+
+ // unbind the mouseOnToPopup events if they were set
+ tipElement.off(EVENT_NAMESPACE);
+
+ // fade out
+ tipElement.fadeOut(options.fadeOutTime, function fadeOutCallback() {
+ var coords = new CSSCoordinates();
+
+ // reset session and tooltip element
+ session.activeHover = null;
+ session.isClosing = false;
+ session.isFixedTipOpen = false;
+ tipElement.removeClass();
+
+ // support mouse-follow and fixed position tips at the same time by
+ // moving the tooltip to the last cursor location after it is hidden
+ coords.set('top', session.currentY + options.offset);
+ coords.set('left', session.currentX + options.offset);
+ tipElement.css(coords);
+
+ // trigger powerTipClose event
+ element.trigger('powerTipClose');
+ });
+ }
+
+ /**
+ * Moves the tooltip to the users mouse cursor.
+ * @private
+ */
+ function positionTipOnCursor() {
+ // to support having fixed tooltips on the same page as cursor tooltips,
+ // where both instances are referencing the same tooltip element, we
+ // need to keep track of the mouse position constantly, but we should
+ // only set the tip location if a fixed tip is not currently open, a tip
+ // open is imminent or active, and the tooltip element in question does
+ // have a mouse-follow using it.
+ if (!session.isFixedTipOpen && (session.isTipOpen || (session.tipOpenImminent && tipElement.data(DATA_HASMOUSEMOVE)))) {
+ // grab measurements
+ var tipWidth = tipElement.outerWidth(),
+ tipHeight = tipElement.outerHeight(),
+ coords = new CSSCoordinates(),
+ collisions,
+ collisionCount;
+
+ // grab collisions
+ coords.set('top', session.currentY + options.offset);
+ coords.set('left', session.currentX + options.offset);
+ collisions = getViewportCollisions(
+ coords,
+ tipWidth,
+ tipHeight
+ );
+
+ // handle tooltip view port collisions
+ if (collisions !== Collision.none) {
+ collisionCount = countFlags(collisions);
+ if (collisionCount === 1) {
+ // if there is only one collision (bottom or right) then
+ // simply constrain the tooltip to the view port
+ if (collisions === Collision.right) {
+ coords.set('left', session.windowWidth - tipWidth);
+ } else if (collisions === Collision.bottom) {
+ coords.set('top', session.scrollTop + session.windowHeight - tipHeight);
+ }
+ } else {
+ // if the tooltip has more than one collision then it is
+ // trapped in the corner and should be flipped to get it out
+ // of the users way
+ coords.set('left', session.currentX - tipWidth - options.offset);
+ coords.set('top', session.currentY - tipHeight - options.offset);
+ }
+ }
+
+ // position the tooltip
+ tipElement.css(coords);
+ }
+ }
+
+ /**
+ * Sets the tooltip to the correct position relative to the specified target
+ * element. Based on options settings.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ */
+ function positionTipOnElement(element) {
+ var priorityList,
+ finalPlacement;
+
+ if (options.smartPlacement) {
+ priorityList = $.fn.powerTip.smartPlacementLists[options.placement];
+
+ // iterate over the priority list and use the first placement option
+ // that does not collide with the view port. if they all collide
+ // then the last placement in the list will be used.
+ $.each(priorityList, function(idx, pos) {
+ // place tooltip and find collisions
+ var collisions = getViewportCollisions(
+ placeTooltip(element, pos),
+ tipElement.outerWidth(),
+ tipElement.outerHeight()
+ );
+
+ // update the final placement variable
+ finalPlacement = pos;
+
+ // break if there were no collisions
+ if (collisions === Collision.none) {
+ return false;
+ }
+ });
+ } else {
+ // if we're not going to use the smart placement feature then just
+ // compute the coordinates and do it
+ placeTooltip(element, options.placement);
+ finalPlacement = options.placement;
+ }
+
+ // add placement as class for CSS arrows
+ tipElement.removeClass('w nw sw e ne se n s w se-alt sw-alt ne-alt nw-alt');
+ tipElement.addClass(finalPlacement);
+ }
+
+ /**
+ * Sets the tooltip position to the appropriate values to show the tip at
+ * the specified placement. This function will iterate and test the tooltip
+ * to support elastic tooltips.
+ * @private
+ * @param {jQuery} element The element that the tooltip should target.
+ * @param {string} placement The placement for the tooltip.
+ * @return {CSSCoordinates} A CSSCoordinates object with the top, left, and
+ * right position values.
+ */
+ function placeTooltip(element, placement) {
+ var iterationCount = 0,
+ tipWidth,
+ tipHeight,
+ coords = new CSSCoordinates();
+
+ // set the tip to 0,0 to get the full expanded width
+ coords.set('top', 0);
+ coords.set('left', 0);
+ tipElement.css(coords);
+
+ // to support elastic tooltips we need to check for a change in the
+ // rendered dimensions after the tooltip has been positioned
+ do {
+ // grab the current tip dimensions
+ tipWidth = tipElement.outerWidth();
+ tipHeight = tipElement.outerHeight();
+
+ // get placement coordinates
+ coords = placementCalculator.compute(
+ element,
+ placement,
+ tipWidth,
+ tipHeight,
+ options.offset
+ );
+
+ // place the tooltip
+ tipElement.css(coords);
+ } while (
+ // sanity check: limit to 5 iterations, and...
+ ++iterationCount <= 5 &&
+ // try again if the dimensions changed after placement
+ (tipWidth !== tipElement.outerWidth() || tipHeight !== tipElement.outerHeight())
+ );
+
+ return coords;
+ }
+
+ /**
+ * Checks for a tooltip desync and closes the tooltip if one occurs.
+ * @private
+ */
+ function closeDesyncedTip() {
+ var isDesynced = false;
+ // It is possible for the mouse cursor to leave an element without
+ // firing the mouseleave or blur event. This most commonly happens when
+ // the element is disabled under mouse cursor. If this happens it will
+ // result in a desynced tooltip because the tooltip was never asked to
+ // close. So we should periodically check for a desync situation and
+ // close the tip if such a situation arises.
+ if (session.isTipOpen && !session.isClosing && !session.delayInProgress && ($.inArray('mouseleave', options.closeEvents) > -1 || $.inArray('mouseout', options.closeEvents) > -1 || $.inArray('blur', options.closeEvents) > -1 || $.inArray('focusout', options.closeEvents) > -1)) {
+ // user moused onto another tip or active hover is disabled
+ if (session.activeHover.data(DATA_HASACTIVEHOVER) === false || session.activeHover.is(':disabled')) {
+ isDesynced = true;
+ } else {
+ // hanging tip - have to test if mouse position is not over the
+ // active hover and not over a tooltip set to let the user
+ // interact with it.
+ // for keyboard navigation: this only counts if the element does
+ // not have focus.
+ // for tooltips opened via the api: we need to check if it has
+ // the forcedOpen flag.
+ if (!isMouseOver(session.activeHover) && !session.activeHover.is(':focus') && !session.activeHover.data(DATA_FORCEDOPEN)) {
+ if (tipElement.data(DATA_MOUSEONTOTIP)) {
+ if (!isMouseOver(tipElement)) {
+ isDesynced = true;
+ }
+ } else {
+ isDesynced = true;
+ }
+ }
+ }
+
+ if (isDesynced) {
+ // close the desynced tip
+ hideTip(session.activeHover);
+ }
+ }
+ }
+
+ // expose methods
+ this.showTip = beginShowTip;
+ this.hideTip = hideTip;
+ this.resetPosition = positionTipOnElement;
+ }
+
+ /**
+ * Determine whether a jQuery object is an SVG element
+ * @private
+ * @param {jQuery} element The element to check
+ * @return {boolean} Whether this is an SVG element
+ */
+ function isSvgElement(element) {
+ return Boolean(window.SVGElement && element[0] instanceof SVGElement);
+ }
+
+ /**
+ * Determines if the specified jQuery.Event object has mouse data.
+ * @private
+ * @param {jQuery.Event=} event The jQuery.Event object to test.
+ * @return {boolean} True if there is mouse data, otherwise false.
+ */
+ function isMouseEvent(event) {
+ return Boolean(event && typeof event.pageX === 'number');
+ }
+
+ /**
+ * Initializes the viewport dimension cache and hooks up the mouse position
+ * tracking and viewport dimension tracking events.
+ * Prevents attaching the events more than once.
+ * @private
+ */
+ function initTracking() {
+ if (!session.mouseTrackingActive) {
+ session.mouseTrackingActive = true;
+
+ // grab the current viewport dimensions on load
+ getViewportDimensions();
+ $(getViewportDimensions);
+
+ // hook mouse move tracking
+ $document.on('mousemove' + EVENT_NAMESPACE, trackMouse);
+
+ // hook viewport dimensions tracking
+ $window.on('resize' + EVENT_NAMESPACE, trackResize);
+ $window.on('scroll' + EVENT_NAMESPACE, trackScroll);
+ }
+ }
+
+ /**
+ * Updates the viewport dimensions cache.
+ * @private
+ */
+ function getViewportDimensions() {
+ session.scrollLeft = $window.scrollLeft();
+ session.scrollTop = $window.scrollTop();
+ session.windowWidth = $window.width();
+ session.windowHeight = $window.height();
+ }
+
+ /**
+ * Updates the window size info in the viewport dimensions cache.
+ * @private
+ */
+ function trackResize() {
+ session.windowWidth = $window.width();
+ session.windowHeight = $window.height();
+ }
+
+ /**
+ * Updates the scroll offset info in the viewport dimensions cache.
+ * @private
+ */
+ function trackScroll() {
+ var x = $window.scrollLeft(),
+ y = $window.scrollTop();
+ if (x !== session.scrollLeft) {
+ session.currentX += x - session.scrollLeft;
+ session.scrollLeft = x;
+ }
+ if (y !== session.scrollTop) {
+ session.currentY += y - session.scrollTop;
+ session.scrollTop = y;
+ }
+ }
+
+ /**
+ * Saves the current mouse coordinates to the session object.
+ * @private
+ * @param {jQuery.Event} event The mousemove event for the document.
+ */
+ function trackMouse(event) {
+ session.currentX = event.pageX;
+ session.currentY = event.pageY;
+ }
+
+ /**
+ * Tests if the mouse is currently over the specified element.
+ * @private
+ * @param {jQuery} element The element to check for hover.
+ * @return {boolean}
+ */
+ function isMouseOver(element) {
+ // use getBoundingClientRect() because jQuery's width() and height()
+ // methods do not work with SVG elements
+ // compute width/height because those properties do not exist on the object
+ // returned by getBoundingClientRect() in older versions of IE
+ var elementPosition = element.offset(),
+ elementBox = element[0].getBoundingClientRect(),
+ elementWidth = elementBox.right - elementBox.left,
+ elementHeight = elementBox.bottom - elementBox.top;
+
+ return session.currentX >= elementPosition.left &&
+ session.currentX <= elementPosition.left + elementWidth &&
+ session.currentY >= elementPosition.top &&
+ session.currentY <= elementPosition.top + elementHeight;
+ }
+
+ /**
+ * Fetches the tooltip content from the specified element's data attributes.
+ * @private
+ * @param {jQuery} element The element to get the tooltip content for.
+ * @return {(string|jQuery|undefined)} The text/HTML string, jQuery object, or
+ * undefined if there was no tooltip content for the element.
+ */
+ function getTooltipContent(element) {
+ var tipText = element.data(DATA_POWERTIP),
+ tipObject = element.data(DATA_POWERTIPJQ),
+ tipTarget = element.data(DATA_POWERTIPTARGET),
+ targetElement,
+ content;
+
+ if (tipText) {
+ if ($.isFunction(tipText)) {
+ tipText = tipText.call(element[0]);
+ }
+ content = tipText;
+ } else if (tipObject) {
+ if ($.isFunction(tipObject)) {
+ tipObject = tipObject.call(element[0]);
+ }
+ if (tipObject.length > 0) {
+ content = tipObject.clone(true, true);
+ }
+ } else if (tipTarget) {
+ targetElement = $('#' + tipTarget);
+ if (targetElement.length > 0) {
+ content = targetElement.html();
+ }
+ }
+
+ return content;
+ }
+
+ /**
+ * Finds any viewport collisions that an element (the tooltip) would have if it
+ * were absolutely positioned at the specified coordinates.
+ * @private
+ * @param {CSSCoordinates} coords Coordinates for the element.
+ * @param {number} elementWidth Width of the element in pixels.
+ * @param {number} elementHeight Height of the element in pixels.
+ * @return {number} Value with the collision flags.
+ */
+ function getViewportCollisions(coords, elementWidth, elementHeight) {
+ var viewportTop = session.scrollTop,
+ viewportLeft = session.scrollLeft,
+ viewportBottom = viewportTop + session.windowHeight,
+ viewportRight = viewportLeft + session.windowWidth,
+ collisions = Collision.none;
+
+ if (coords.top < viewportTop || Math.abs(coords.bottom - session.windowHeight) - elementHeight < viewportTop) {
+ collisions |= Collision.top;
+ }
+ if (coords.top + elementHeight > viewportBottom || Math.abs(coords.bottom - session.windowHeight) > viewportBottom) {
+ collisions |= Collision.bottom;
+ }
+ if (coords.left < viewportLeft || coords.right + elementWidth > viewportRight) {
+ collisions |= Collision.left;
+ }
+ if (coords.left + elementWidth > viewportRight || coords.right < viewportLeft) {
+ collisions |= Collision.right;
+ }
+
+ return collisions;
+ }
+
+ /**
+ * Counts the number of bits set on a flags value.
+ * @param {number} value The flags value.
+ * @return {number} The number of bits that have been set.
+ */
+ function countFlags(value) {
+ var count = 0;
+ while (value) {
+ value &= value - 1;
+ count++;
+ }
+ return count;
+ }
+
+// return api for commonjs and amd environments
+ return $.powerTip;
+}));
diff --git a/i/jquery.powertip/jquery.powertip.min.js b/i/jquery.powertip/jquery.powertip.min.js
new file mode 100644
index 0000000..9f20856
--- /dev/null
+++ b/i/jquery.powertip/jquery.powertip.min.js
@@ -0,0 +1,8 @@
+/*!
+ PowerTip v1.3.0 (2017-01-15)
+ https://stevenbenner.github.io/jquery-powertip/
+ Copyright (c) 2017 Steven Benner (http://stevenbenner.com/).
+ Released under MIT license.
+ https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt
+*/
+!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){function b(){var b=this;b.top="auto",b.left="auto",b.right="auto",b.bottom="auto",b.set=function(c,d){a.isNumeric(d)&&(b[c]=Math.round(d))}}function c(a,b,c){function d(d,e){g(),a.data(u)?h():d?(e&&a.data(v,!0),i(),c.showTip(a)):(E.tipOpenImminent=!0,k=setTimeout(function(){k=null,f()},b.intentPollInterval))}function e(d){l&&(l=E.closeDelayTimeout=clearTimeout(l),E.delayInProgress=!1),g(),E.tipOpenImminent=!1,a.data(u)&&(a.data(v,!1),d?c.hideTip(a):(E.delayInProgress=!0,E.closeDelayTimeout=setTimeout(function(){E.closeDelayTimeout=null,c.hideTip(a),E.delayInProgress=!1,l=null},b.closeDelay),l=E.closeDelayTimeout))}function f(){var e=Math.abs(E.previousX-E.currentX),f=Math.abs(E.previousY-E.currentY),g=e+f;g-1||a.inArray("mouseout",c.closeEvents)>-1||a.inArray("blur",c.closeEvents)>-1||a.inArray("focusout",c.closeEvents)>-1)&&(E.activeHover.data(u)===!1||E.activeHover.is(":disabled")?b=!0:m(E.activeHover)||E.activeHover.is(":focus")||E.activeHover.data(v)||(y.data(x)?m(y)||(b=!0):b=!0),b&&g(E.activeHover))}var l=new d,y=a("#"+c.popupId);0===y.length&&(y=a("",{id:c.popupId}),0===s.length&&(s=a("body")),s.append(y),E.tooltips=E.tooltips?E.tooltips.add(y):y),c.followMouse&&(y.data(w)||(q.on("mousemove"+C,h),r.on("scroll"+C,h),y.data(w,!0))),this.showTip=e,this.hideTip=g,this.resetPosition=i}function f(a){return Boolean(window.SVGElement&&a[0]instanceof SVGElement)}function g(a){return Boolean(a&&"number"==typeof a.pageX)}function h(){E.mouseTrackingActive||(E.mouseTrackingActive=!0,i(),a(i),q.on("mousemove"+C,l),r.on("resize"+C,j),r.on("scroll"+C,k))}function i(){E.scrollLeft=r.scrollLeft(),E.scrollTop=r.scrollTop(),E.windowWidth=r.width(),E.windowHeight=r.height()}function j(){E.windowWidth=r.width(),E.windowHeight=r.height()}function k(){var a=r.scrollLeft(),b=r.scrollTop();a!==E.scrollLeft&&(E.currentX+=a-E.scrollLeft,E.scrollLeft=a),b!==E.scrollTop&&(E.currentY+=b-E.scrollTop,E.scrollTop=b)}function l(a){E.currentX=a.pageX,E.currentY=a.pageY}function m(a){var b=a.offset(),c=a[0].getBoundingClientRect(),d=c.right-c.left,e=c.bottom-c.top;return E.currentX>=b.left&&E.currentX<=b.left+d&&E.currentY>=b.top&&E.currentY<=b.top+e}function n(b){var c,d,e=b.data(z),f=b.data(A),g=b.data(B);return e?(a.isFunction(e)&&(e=e.call(b[0])),d=e):f?(a.isFunction(f)&&(f=f.call(b[0])),f.length>0&&(d=f.clone(!0,!0))):g&&(c=a("#"+g),c.length>0&&(d=c.html())),d}function o(a,b,c){var d=E.scrollTop,e=E.scrollLeft,f=d+E.windowHeight,g=e+E.windowWidth,h=F.none;return(a.topf||Math.abs(a.bottom-E.windowHeight)>f)&&(h|=F.bottom),(a.leftg)&&(h|=F.left),(a.left+b>g||a.right-1?j.on(c+C,function(b){a.powerTip.toggle(this,b)}):j.on(c+C,function(b){a.powerTip.show(this,b)})}),a.each(f.closeEvents,function(b,c){a.inArray(c,f.openEvents)<0&&j.on(c+C,function(b){a.powerTip.hide(this,!g(b))})}),j.on("keydown"+C,function(b){27===b.keyCode&&a.powerTip.hide(this,!0)})),E.elements=E.elements?E.elements.add(j):j,j):j},a.fn.powerTip.defaults={fadeInTime:200,fadeOutTime:100,followMouse:!1,popupId:"powerTip",popupClass:null,intentSensitivity:7,intentPollInterval:100,closeDelay:100,placement:"n",smartPlacement:!1,offset:10,mouseOnToPopup:!1,manual:!1,openEvents:["mouseenter","focus"],closeEvents:["mouseleave","blur"]},a.fn.powerTip.smartPlacementLists={n:["n","ne","nw","s"],e:["e","ne","se","w","nw","sw","n","s","e"],s:["s","se","sw","n"],w:["w","nw","sw","e","ne","se","n","s","w"],nw:["nw","w","sw","n","s","se","nw"],ne:["ne","e","se","n","s","sw","ne"],sw:["sw","w","nw","s","n","ne","sw"],se:["se","e","ne","s","n","nw","se"],"nw-alt":["nw-alt","n","ne-alt","sw-alt","s","se-alt","w","e"],"ne-alt":["ne-alt","n","nw-alt","se-alt","s","sw-alt","e","w"],"sw-alt":["sw-alt","s","se-alt","nw-alt","n","ne-alt","w","e"],"se-alt":["se-alt","s","sw-alt","ne-alt","n","nw-alt","e","w"]},a.powerTip={show:function(b,c){return g(c)?(l(c),E.previousX=c.pageX,E.previousY=c.pageY,a(b).data(t).show()):a(b).first().data(t).show(!0,!0),b},reposition:function(b){return a(b).first().data(t).resetPosition(),b},hide:function(b,c){var d;return c=!b||c,b?d=a(b).first().data(t):E.activeHover&&(d=E.activeHover.data(t)),d&&d.hide(c),b},toggle:function(b,c){return E.activeHover&&E.activeHover.is(b)?a.powerTip.hide(b,!g(c)):a.powerTip.show(b,c),b},destroy:function(b){var c=b?a(b):E.elements;return E.elements&&0!==E.elements.length?(c.off(C).each(function(){var b=a(this),c=[y,t,u,v];b.data(y)&&(b.attr("title",b.data(y)),c.push(z)),b.removeData(c)}),E.elements=E.elements.not(c),0===E.elements.length&&(r.off(C),q.off(C),E.mouseTrackingActive=!1,E.tooltips.remove(),E.tooltips=null),b):b}},a.powerTip.showTip=a.powerTip.show,a.powerTip.closeTip=a.powerTip.hide,a.powerTip});
\ No newline at end of file
diff --git a/i/main.js b/i/main.js
index 1788500..1d5fc4a 100644
--- a/i/main.js
+++ b/i/main.js
@@ -1,3 +1,8 @@
+/*
+TODO: Auto-reconnect web sockets
+TODO: "Click" the "Jog Control" tab when the config reloads (just in case)
+*/
+
/*
GRBLWeb - a web based CNC controller for GRBL
@@ -25,178 +30,193 @@
*/
-$(document).ready(function() {
-
- // init vars for better controls
- var isMouseDown = false;
- var tsLast = Date.now();
-
- // get dimensions for better controls
- var betterWidth = $('#betterControls').width();
- var betterHeight = $('#betterControls').height();
- var bPointWidth = $('#betterControlsPoint').width();
- var bPointHeight = $('#betterControlsPoint').height();
-
- // track the control mouse position externally
- var betterX = 0;
- var betterY = 0;
-
- // get the scale factor to reduce x and y to a resolution of -1 to 1
- var xSf = 2/betterWidth;
- var ySf = 2/betterHeight;
-
- // center the control point
- $('#betterControlsPoint').css('top', (betterHeight/2)-(bPointHeight/2) + 'px');
- $('#betterControlsPoint').css('left', (betterWidth/2)-(bPointWidth/2) + 'px');
-
- // on mousedown, set isMouseDown to true
- $('#betterControls').mousedown(function(event) {
- event.preventDefault();
- isMouseDown = true;
- });
- document.getElementById('betterControls').addEventListener('touchstart', function(event) {
- event.preventDefault();
- isMouseDown = true;
- }, false);
-
- // on mouseup reset center point
- $('#betterControls').mouseup(function(event) {
- event.preventDefault();
- isMouseDown = false;
- $('#betterControlsPoint').css('top', (betterHeight/2)-(bPointHeight/2) + 'px');
- $('#betterControlsPoint').css('left', (betterWidth/2)-(bPointWidth/2) + 'px');
- });
- document.getElementById('betterControls').addEventListener('touchend', function(event) {
- event.preventDefault();
- isMouseDown = false;
- $('#betterControlsPoint').css('top', (betterHeight/2)-(bPointHeight/2) + 'px');
- $('#betterControlsPoint').css('left', (betterWidth/2)-(bPointWidth/2) + 'px');
- });
-
- // loop for bettercontrol
- setInterval(function() {
- if (isMouseDown) {
-
- var gcX = betterX-(betterWidth/2);
- var gcY = betterY-(betterHeight/2);
-
- // add the scale factors
- gcX = xSf*gcX;
- gcY = ySf*gcY;
-
- // invert y axis because JS and CNC are opposite there
- if (gcY < 0) {
- gcY = Math.abs(gcY);
- } else if (gcY > 0) {
- gcY = -gcY;
- }
-
- // first get speed, calculated from the mean of abs(x) and abs(y)
- //var fSpeed = (Math.abs(gcX)+Math.abs(gcY))/2;
-
- // first get speed, calculated from the highest abs of x and y
- if (Math.abs(gcX) > Math.abs(gcY)) {
- var fSpeed = Math.abs(gcX)*$('#jogSpeed').val();
- } else {
- var fSpeed = Math.abs(gcY)*$('#jogSpeed').val();
- }
-
- fSpeed = Math.round(fSpeed*100)/100;
-
- // set final position movements based on #jogSize
- gcX = Math.round(gcX*$('#jogSize').val()*1000)/1000;
- gcY = Math.round(gcY*$('#jogSize').val()*1000)/1000;
-
- // gcode to send
- socket.emit('gcodeLine', { line: 'G91\nG0 F'+fSpeed+' X'+gcX+' Y'+gcY+'\nG90\n'});
- }
- }, 200);
-
- // on mousemove send gcode
- $('#betterControls').mousemove(function(event) {
- if (isMouseDown) {
- betterX = event.pageX-this.offsetLeft;
- betterY = event.pageY-this.offsetTop;
-
- // move point
- $('#betterControlsPoint').css('top',betterY-(bPointHeight/2) + 'px');
- $('#betterControlsPoint').css('left',betterX-(bPointWidth/2) + 'px');
-
- }
- });
- document.getElementById('betterControls').addEventListener('touchmove', function(event) {
- event.preventDefault();
- if (isMouseDown) {
- betterX = event.pageX-this.offsetLeft;
- betterY = event.pageY-this.offsetTop;
-
- // move point
- $('#betterControlsPoint').css('top',betterY-(bPointHeight/2) + 'px');
- $('#betterControlsPoint').css('left',betterX-(bPointWidth/2) + 'px');
-
- }
- });
+var lastUnitsOfMeasurement = '';
+var unitsBeforeProbe = '';
+var config = {};
+$(document).ready(function() {
$( window ).resize(function() {
// when header resizes, move ui down
$('.table-layout').css('margin-top',$('.navbar-collapse').height()-34);
});
+ // Set up powertip and optional location
+ $('[data-powertip]').each(function() {
+ var opts = {};
+
+ if($(this).data('powertippos')!=undefined)
+ opts.placement = $(this).data('powertippos');
+
+ $(this).powerTip(opts);
+ });
+
var socket = io.connect('');
socket.on('serverError', function (data) {
- alert(data);
+ console.log(data);
});
+
socket.on('gcodeFromJscut', function (data) {
$('#command').val(data.val);
openGCodeFromText();
alert('new data from jscut');
});
+
// config from server
socket.on('config', function (data) {
+ console.log('config', data);
+ config = data;
+
if (data.showWebCam == true) {
// show the webcam and link
-
var webroot = window.location.protocol+'//'+window.location.hostname;
- //console.log(webroot);
$('#wcImg').attr('src', webroot+':'+data.webcamPort+'/?action=stream');
-
$('#wcLink').attr('href', webroot+':'+data.webcamPort+'/javascript_simple.html');
-
$('#webcam').css('display','inline-block');
}
+
+ // Hide the jsCut button if jsCut is "disabled"
+ if(data.enableJsCut==0) {
+ $('#jsCutButton').hide();
+ }
+
+ // Show controls tabs to "enable" probe controls
+ if(data.enableProbeControls==1) {
+ $('#controlTabs').show();
+ }
+
+ // Hide "better controls" and show simple controls
+ if(data.enableSimpleControls==1) {
+ $('#betterControlsWrapper').hide();
+ $('#simpleControls').show();
+ $('#probeControls').hide();
+ }
+
+ // Fill in jog default step increment
+ if(data.jogControlDefaultIncr!=undefined)
+ $('#jogSize').val(data.jogControlDefaultIncr).attr('placeholder', data.jogControlDefaultIncr);
+
+ // Fill in jog default feed rate
+ if(data.jogControlDefaultFeed!=undefined)
+ $('#jogSpeed').val(data.jogControlDefaultFeed).attr('placeholder', data.jogControlDefaultFeed);
+
+ // Fill in default X offset for probe
+ if(data.probeControlXOffset!=undefined)
+ $('#probeOffsetX').val(data.probeControlXOffset).attr('placeholder', data.probeControlXOffset);
+
+ // Fill in default Y offset for probe
+ if(data.probeControlYOffset!=undefined)
+ $('#probeOffsetY').val(data.probeControlYOffset).attr('placeholder', data.probeControlYOffset);
+
+ // Fill in default Z offset for probe
+ if(data.probeControlZOffset!=undefined)
+ $('#probeOffsetZ').val(data.probeControlZOffset).attr('placeholder', data.probeControlZOffset);
+
+ // "Click" the "Jog Control" tab
+ $('#controlTabLink').click().parent().click();
});
+
socket.on('ports', function (data) {
- //console.log('ports event',data);
$('#choosePort').html('');
for (var i=0; i'+data[i].comName+':'+data[i].pnpId+'');
+ var selected = '';
+
+ if(data[i].pnpId!=undefined && data[i].pnpId.toUpperCase().indexOf("ARDUINO_UNO")>=0)
+ selected = 'selected="selected"';
+
+ $('#choosePort').append('');
}
if (data.length == 1) {
$('#choosePort').val('0');
$('#choosePort').change();
}
+
+ $('#choosePort').change();
});
+
socket.on('qStatus', function (data) {
$('#qStatus').html(data.currentLength+'/'+data.currentMax);
+
+ var pct = Math.round((data.currentMax-data.currentLength)/data.currentMax*100);
+ if(isNaN(pct))
+ pct = 100;
+
+ if(data.currentMax==0)
+ pct = 0;
+
+ $('#queueProgress').attr('aria-valuenow', pct).width(pct+'%').text(pct+'%');
+
+ if(pct>0)
+ $('#queueProgress').addClass('active');
+ else
+ $('#queueProgress').removeClass('active');
});
+
socket.on('machineStatus', function (data) {
+ if(data.status.toUpperCase()=='ALARM') {
+ data.status = ''+data.status+'';
+ }
+
+ // $('#console').append('
'+JSON.stringify(data)+'
');
+ // data.status = ''+data+'';
+
$('#mStatus').html(data.status);
+
+ // Convert machine coordinates from mm to inches
+ if(data.unitsOfMeasurement=='in') {
+ for(var i in data) {
+ if(i.indexOf("pos")>=0) {
+ for(var j in data[i]) {
+ data[i][j] = (Math.round(parseFloat(data[i][j]/25.4) * 10000)/10000);;
+ }
+ }
+ }
+ }
+
$('#mX').html('X: '+data.mpos[0]);
$('#mY').html('Y: '+data.mpos[1]);
$('#mZ').html('Z: '+data.mpos[2]);
$('#wX').html('X: '+data.wpos[0]);
$('#wY').html('Y: '+data.wpos[1]);
$('#wZ').html('Z: '+data.wpos[2]);
- //console.log(data);
+
+ // Only attempt to change things if the unit of measurement has changed
+ if(data.unitsOfMeasurement!=lastUnitsOfMeasurement) {
+ if (config.showUnitsBackdrop)
+ $('.unitsOfMeasurementText').text(data.unitsOfMeasurement);
+
+ // Values for jog options
+ var step_incr = parseFloat($('#jogSize').val());
+ var step_feed = parseFloat($('#jogSpeed').val());
+
+ if(data.unitsOfMeasurement.toUpperCase()=='IN') {
+ $('#setInches').addClass('btn-primary');
+ $('#setMillimeters').removeClass('btn-primary');
+
+ $('#jogSize').val(step_incr/25.4);
+ $('#jogSpeed').val(step_feed/25.4);
+ } else if(data.unitsOfMeasurement.toUpperCase()=='MM') {
+ $('#setInches').removeClass('btn-primary');
+ $('#setMillimeters').addClass('btn-primary');
+
+ // Don't do any conversion if this is the first page load
+ if(lastUnitsOfMeasurement!='') {
+ $('#jogSize').val(step_incr*25.4);
+ $('#jogSpeed').val(step_feed*25.4);
+ }
+ }
+
+ lastUnitsOfMeasurement = data.unitsOfMeasurement;
+ }
});
+
socket.on('serialRead', function (data) {
if ($('#console p').length > 300) {
// remove oldest if already at 300 lines
@@ -206,6 +226,28 @@ $(document).ready(function() {
$('#console').scrollTop($("#console")[0].scrollHeight - $("#console").height());
});
+
+ socket.on('sensors', function(data) {
+ if(config.enablePiTemperature==1) {
+ var tempHtml = '';
+
+ for(var i in data) {
+ if(config.piTemperatureFahrenheit==1) {
+ tempHtml += Math.round(parseFloat(data[i]) * 9/5 + 32)+'°F, ';
+ } else {
+ tempHtml += (Math.round(parseFloat(data[i]) * 100)/100) +'°C, ';
+ }
+ }
+
+ // Cut off the last ", "
+ tempHtml = tempHtml.trim();
+ tempHtml = tempHtml.substr(0, tempHtml.length-7);
+
+ $('#mTemp').html(tempHtml);
+ }
+ })
+
+
$('#choosePort').on('change', function() {
// select port
socket.emit('usePort', $('#choosePort').val());
@@ -258,15 +300,140 @@ $(document).ready(function() {
$('#mPosition').hide();
});
+ $('#probeTabLink').on('click', function() {
+ $('#probeTab').addClass('active');
+ $('#controlTab').removeClass('active');
+ $('#probeControls').show();
+ $('#simpleControls').hide();
+ });
+
+ $('#controlTabLink').on('click', function() {
+ $('#controlTab').addClass('active');
+ $('#probeTab').removeClass('active');
+ $('#simpleControls').show();
+ $('#probeControls').hide();
+ });
+
+ $('#probeHomeZero').on('click', function() {
+ socket.emit('gcodeLine', { line: '$H' });
+ socket.emit('gcodeLine', { line: 'G92 X0 Y0 Z0' });
+ });
+
+ $('#probeX').on('click', function() {
+ var offsetX = parseFloat($('#probeOffsetX').val())+parseFloat($('#probeBitDiameter').val()/2);
+
+ // Remember the units of measurement before we start the probe
+ unitsBeforeProbe = lastUnitsOfMeasurement;
+
+ socket.emit('gcodeLine', { line: 'G21' }); // Set to mm
+ socket.emit('gcodeLine', { line: 'G38.2 X-50 F20' }); // Probe X
+ socket.emit('gcodeLine', { line: 'G92 X'+(offsetX*-1) }); // Set X offset
+ socket.emit('gcodeLine', { line: 'G1 X'+(offsetX+2)+' F1000' }); // Move probe 2mm away from offset
+
+ // Restore the units of measurement after the probe
+ if(unitsBeforeProbe=='in')
+ socket.emit('gcodeLine', { line: 'G20' });
+ });
+
+ $('#probeY').on('click', function() {
+ var offsetY = parseFloat($('#probeOffsetY').val())+parseFloat($('#probeBitDiameter').val()/2);
+
+ // Remember the units of measurement before we start the probe
+ unitsBeforeProbe = lastUnitsOfMeasurement;
+
+ socket.emit('gcodeLine', { line: 'G21' }); // Set to mm
+ socket.emit('gcodeLine', { line: 'G38.2 Y-50 F20' }); // Probe Y
+ socket.emit('gcodeLine', { line: 'G92 Y'+(offsetY*-1) }); // Set Y offset
+ socket.emit('gcodeLine', { line: 'G1 Y'+(offsetY+2)+' F1000' }); // Move probe 2mm away from offset
+
+ // Restore the units of measurement after the probe
+ if(unitsBeforeProbe=='in')
+ socket.emit('gcodeLine', { line: 'G20' });
+ });
+
+ $('#probeZ').on('click', function() {
+ var offsetZ = parseFloat($('#probeOffsetZ').val());
+
+ // Remember the units of measurement before we start the probe
+ unitsBeforeProbe = lastUnitsOfMeasurement;
+
+ socket.emit('gcodeLine', { line: 'G21' }); // Set to mm
+ socket.emit('gcodeLine', { line: 'G38.2 Z-75 F20'}); // Probe Z
+ socket.emit('gcodeLine', { line: 'G92 Z'+offsetZ }); // Set Z offset
+ socket.emit('gcodeLine', { line: 'G1 Z'+(offsetZ+5)+' F1000' }); // Move probe 5mm away from offset
+
+ // Restore the units of measurement after the probe
+ if(unitsBeforeProbe=='in')
+ socket.emit('gcodeLine', { line: 'G20' });
+ });
+
+ $('#probeAll').on('click', function() {
+ var offsetX = parseFloat($('#probeOffsetX').val())+parseFloat($('#probeBitDiameter').val()/2)+10;
+ var offsetY = parseFloat($('#probeOffsetY').val())+parseFloat($('#probeBitDiameter').val()/2)+10;
+ var offsetZ = parseFloat($('#probeOffsetZ').val())+10;
+
+ $('#probeZ').click(); // Probe Z
+ $('#probeX').click(); // Probe X
+ $('#probeY').click(); // Probe Y
+
+ // Remember the units of measurement before we start the probe
+ unitsBeforeProbe = lastUnitsOfMeasurement;
+
+ socket.emit('gcodeLine', { line: 'G21' });
+ socket.emit('gcodeLine', { line: 'G1 X'+offsetX+' Y'+offsetY+' Z'+offsetZ+' F1000' }); // Move probe away from offset
+
+ // Restore the units of measurement after the probe
+ if(unitsBeforeProbe=='in')
+ socket.emit('gcodeLine', { line: 'G20' });
+ });
+
+ $('#gotoZeroZeroZero').on('click', function() {
+ socket.emit('gcodeLine', { line: 'G1 X0 Y0 Z0 F500' });
+ });
+
$('#sendZero').on('click', function() {
socket.emit('gcodeLine', { line: 'G92 X0 Y0 Z0' });
+ socket.emit('gcodeLine', { line: 'G28.1' });
});
- $('#sendCommand').on('click', function() {
+ $('#sendZeroX').on('click', function() {
+ socket.emit('gcodeLine', { line: 'G92 X0' });
+ socket.emit('gcodeLine', { line: 'G28.1 X0' });
+ });
+
+ $('#sendZeroY').on('click', function() {
+ socket.emit('gcodeLine', { line: 'G92 Y0' });
+ socket.emit('gcodeLine', { line: 'G28.1 Y0' });
+ });
+ $('#sendZeroZ').on('click', function() {
+ socket.emit('gcodeLine', { line: 'G92 Z0' });
+ socket.emit('gcodeLine', { line: 'G28.1 Z0' });
+ });
+
+ $('#setInches').on('click', function() {
+ socket.emit('gcodeLine', { line: 'G20' });
+ });
+
+ $('#setMillimeters').on('click', function() {
+ socket.emit('gcodeLine', { line: 'G21' });
+ });
+
+ $('#sendHome').on('click', function() {
+ socket.emit('gcodeLine', { line: '$H' });
+ });
+
+ $('#sendUnlock').on('click', function() {
+ socket.emit('gcodeLine', { line: '$X' });
+ });
+
+ $('#sendCommand').on('click', function() {
socket.emit('gcodeLine', { line: $('#command').val() });
$('#command').val('');
+ });
+ $('#refreshPorts').on('click', function() {
+ socket.emit('refreshPorts', {});
});
// shift enter for send command
@@ -285,73 +452,107 @@ $(document).ready(function() {
$('#xM').on('click', function() {
socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' X-'+$('#jogSize').val()+'\nG90'});
});
+
$('#xP').on('click', function() {
socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' X'+$('#jogSize').val()+'\nG90'});
});
+
$('#yP').on('click', function() {
socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' Y'+$('#jogSize').val()+'\nG90'});
});
+
$('#yM').on('click', function() {
socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' Y-'+$('#jogSize').val()+'\nG90'});
});
+
$('#zP').on('click', function() {
- socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' Z'+$('#jogSize').val()+'\nG90'});
+ var distance = parseFloat($('#jogSize').val());
+
+ if(lastUnitsOfMeasurement=='in')
+ distance = distance*25.4
+
+ if(distance>=config.maxMoveZ)
+ distance = config.maxMoveZ;
+
+ if(lastUnitsOfMeasurement=='in')
+ distance = distance/25.4;
+
+ // Throw a warning in the console
+ $('#console').append('
!!!!: Only moved '+distance+lastUnitsOfMeasurement+' due to maxMoveZ safety limit
');
+ $('#console').scrollTop($("#console")[0].scrollHeight - $("#console").height());
+
+ socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' Z'+distance+'\nG90'});
});
+
$('#zM').on('click', function() {
- socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' Z-'+$('#jogSize').val()+'\nG90'});
- });
+ var distance = parseFloat($('#jogSize').val());
- // WASD and up/down keys
- $(document).keydown(function (e) {
- var keyCode = e.keyCode || e.which;
+ if(lastUnitsOfMeasurement=='in')
+ distance = distance*25.4
- if ($('#command').is(':focus')) {
- // don't handle keycodes inside command window
- return;
- }
+ if(distance>=config.maxMoveZ)
+ distance = config.maxMoveZ;
- switch (keyCode) {
- case 65:
- // a key X-
- e.preventDefault();
- $('#xM').click();
- break;
- case 68:
- // d key X+
- e.preventDefault();
- $('#xP').click();
- break;
- case 87:
- // w key Y+
- e.preventDefault();
- $('#yP').click();
- break;
- case 83:
- // s key Y-
- e.preventDefault();
- $('#yM').click();
- break;
- case 38:
- // up arrow Z+
- e.preventDefault();
- $('#zP').click();
- break;
- case 40:
- // down arrow Z-
- e.preventDefault();
- $('#zM').click();
- break;
- }
+ if(lastUnitsOfMeasurement=='in')
+ distance = distance/25.4;
+
+ // Throw a warning in the console
+ $('#console').append('
!!!!: Only moved '+distance+lastUnitsOfMeasurement+' due to maxMoveZ safety limit
');
+ $('#console').scrollTop($("#console")[0].scrollHeight - $("#console").height());
+
+ socket.emit('gcodeLine', { line: 'G91\nG1 F'+$('#jogSpeed').val()+' Z-'+distance+'\nG90'});
});
+ // WASD and up/down keys
+ // $(document).keydown(function (e) {
+ // var keyCode = e.keyCode || e.which;
+ //
+ // if ($('#command').is(':focus')) {
+ // // don't handle keycodes inside command window
+ // return;
+ // }
+ //
+ // switch (keyCode) {
+ // case 65:
+ // // a key X-
+ // e.preventDefault();
+ // $('#xM').click();
+ // break;
+ // case 68:
+ // // d key X+
+ // e.preventDefault();
+ // $('#xP').click();
+ // break;
+ // case 87:
+ // // w key Y+
+ // e.preventDefault();
+ // $('#yP').click();
+ // break;
+ // case 83:
+ // // s key Y-
+ // e.preventDefault();
+ // $('#yM').click();
+ // break;
+ // case 38:
+ // // up arrow Z+
+ // e.preventDefault();
+ // $('#zP').click();
+ // break;
+ // case 40:
+ // // down arrow Z-
+ // e.preventDefault();
+ // $('#zM').click();
+ // break;
+ // }
+ // });
+
// handle gcode uploads
if (window.FileReader) {
-
var reader = new FileReader ();
// drag and drop
function dragEvent (ev) {
- ev.stopPropagation ();
+ ev.stopPropagation ();
ev.preventDefault ();
if (ev.type == 'drop') {
reader.onloadend = function (ev) {
@@ -359,7 +560,7 @@ $(document).ready(function() {
openGCodeFromText();
};
reader.readAsText (ev.dataTransfer.files[0]);
- }
+ }
}
document.getElementById('command').addEventListener ('dragenter', dragEvent, false);
@@ -379,5 +580,4 @@ $(document).ready(function() {
} else {
alert('your browser is too old to upload files, get the latest Chromium or Firefox');
}
-
});
diff --git a/server.js b/server.js
index c7aca3f..1021bb4 100644
--- a/server.js
+++ b/server.js
@@ -1,33 +1,39 @@
+/*
+ TODO: Allow saving/loading of probing profiles
+*/
+
/*
- GRBLWeb - a web based CNC controller for GRBL
- Copyright (C) 2015 Andrew Hodel
+ GRBLWeb - a web based CNC controller for GRBL
+ Copyright (C) 2015 Andrew Hodel
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
+//var reload = require('require-reload')(require);
+//var config = reload('./config.js');
var config = require('./config');
var serialport = require("serialport");
-var SerialPort = serialport.SerialPort; // localize object constructor
+var SerialPort = serialport.SerialPort;
var app = require('http').createServer(handler)
, io = require('socket.io').listen(app)
, fs = require('fs');
@@ -40,16 +46,31 @@ var http = require('http');
// test for webcam
config.showWebCam = false;
-http.get('http://127.0.0.1:8080', function(res) {
+
+// Monitor config.js for changes
+/*
+fs.watch('./config.js', function(e, f) {
+ console.log('config.js changed, reloading');
+ console.log('config: '+JSON.stringify(config));
+
+ config = reload('./config.js');
+
+ for(var i in io.sockets.connected)
+ io.sockets.connected[i].emit('config', config);
+});
+*/
+
+
+http.get('http://127.0.0.1:8080/?action=snapshot', function(res) {
// valid response, enable webcam
console.log('enabling webcam');
config.showWebCam = true;
}).on('socket', function(socket) {
// 2 second timeout on this socket
socket.setTimeout(2000);
- socket.on('timeout', function() {
- this.abort();
- });
+ //socket.on('timeout', function() {
+ // this.abort();
+ //});
}).on('error', function(e) {
console.log('Got error: '+e.message+' not enabling webcam')
});
@@ -57,10 +78,8 @@ http.get('http://127.0.0.1:8080', function(res) {
app.listen(config.webPort);
var fileServer = new static.Server('./i');
-function handler (req, res) {
-
- //console.log(req.url);
+function handler (req, res) {
if (req.url.indexOf('/api/uploadGcode') == 0 && req.method == 'POST') {
// this is a gcode upload, probably from jscut
console.log('new data from jscut');
@@ -80,67 +99,78 @@ function handler (req, res) {
});
} else {
fileServer.serve(req, res, function (err, result) {
- if (err) console.log('fileServer error: ',err);
+ if (err) console.log('fileServer error: ', err, req.url);
});
}
}
+
function ConvChar( str ) {
c = {'<':'<', '>':'>', '&':'&', '"':'"', "'":''',
- '#':'#' };
+ '#':'#' };
return str.replace( /[<&>'"#]/g, function(s) { return c[s]; } );
}
+
var sp = [];
var allPorts = [];
+var piTemp = [];
+
+function doSerialPortList() {
+ serialport.list(function (err, ports) {
+ // if on rPi - http://www.hobbytronics.co.uk/raspberry-pi-serial-port
+ if (fs.existsSync('/dev/ttyAMA0') && config.usettyAMA0 == 1) {
+ (ports = ports || []).push({comName:'/dev/ttyAMA0',manufacturer: undefined,pnpId: 'raspberryPi__GPIO'});
+ console.log('adding /dev/ttyAMA0 because it is enabled in config.js, you may need to enable it in the os - http://www.hobbytronics.co.uk/raspberry-pi-serial-port');
+ }
-serialport.list(function (err, ports) {
-
- // if on rPi - http://www.hobbytronics.co.uk/raspberry-pi-serial-port
- if (fs.existsSync('/dev/ttyAMA0') && config.usettyAMA0 == 1) {
- (ports = ports || []).push({comName:'/dev/ttyAMA0',manufacturer: undefined,pnpId: 'raspberryPi__GPIO'});
- console.log('adding /dev/ttyAMA0 because it is enabled in config.js, you may need to enable it in the os - http://www.hobbytronics.co.uk/raspberry-pi-serial-port');
- }
+ allPorts = ports;
+
+ for (var i=0; iRESP: '+data+''});
@@ -191,7 +237,6 @@ function serialData(data, port) {
sp[port].lastSerialWrite.shift();
} else if (data.indexOf('error') == 0) {
-
// error is red
emitToPortSockets(port, 'serialRead', {'line':'RESP: '+data+''});
@@ -208,6 +253,14 @@ function serialData(data, port) {
} else {
// other is grey
emitToPortSockets(port, 'serialRead', {'line':'RESP: '+data+''});
+
+ // This is where we're likely to see the units of measurement response
+ // Inches
+ if(data.indexOf(' G20 ')>=0)
+ unitsOfMeasurement = 'in';
+
+ if(data.indexOf(' G21 ')>=0)
+ unitsOfMeasurement = 'mm';
}
if (sp[port].q.length == 0) {
@@ -222,14 +275,16 @@ function serialData(data, port) {
}
+
var currentSocketPort = {};
-function sendFirstQ(port) {
+function sendFirstQ(port) {
if (sp[port].q.length < 1) {
// nothing to send
return;
}
+
var t = sp[port].q.shift();
// remove any comments after the command
@@ -242,7 +297,8 @@ function sendFirstQ(port) {
sendFirstQ(port);
return;
}
- //console.log('sending '+t+' ### '+sp[port].q.length+' current q length');
+
+ // console.log('sending '+t+' ### '+sp[port].q.length+' current q length');
// loop through all registered port clients
for (var i=0; i=0 || data.line.toUpperCase().indexOf("G21")>=0 || data.line.toUpperCase().indexOf("$X")>=0)
+ data.line += "\n$G";
// valid serial port selected, safe to send
// split newlines
@@ -289,7 +355,6 @@ io.sockets.on('connection', function (socket) {
} else {
socket.emit('serverError', 'you must select a serial port');
}
-
});
socket.on('clearQ', function(data) {
@@ -312,7 +377,6 @@ io.sockets.on('connection', function (socket) {
});
socket.on('disconnect', function() {
-
if (typeof currentSocketPort[socket.id] != 'undefined') {
for (var c=0; c=0 && data.indexOf('YES')>=0) {
+ piTemp[i] = parseFloat(data.substr(data.indexOf('t=')+2).trim())/1000;
+
+ // Maybe the internal temp
+ } else
+ piTemp[i] = parseFloat(data)/1000;
+ }
+
+ emitToAllPortSockets('sensors', piTemp);
+
+ setTimeout(getSensors, 2000);
+}