Integrated eslint and reformatted all of the JS/TS

This commit is contained in:
David Carley
2022-09-02 00:04:31 +00:00
parent 868258cfa7
commit c6a3732750
49 changed files with 8535 additions and 5675 deletions

82
.eslintrc.yml Normal file
View File

@@ -0,0 +1,82 @@
env:
es2021: true
node: true
browser: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/recommended
overrides: []
parser: "@typescript-eslint/parser"
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- "@typescript-eslint"
globals:
Vue: readonly
THREE: readonly
SvelteComponents: readonly
$: readonly
Clusterize: readonly
SockJS: readonly
rules:
indent:
- off
"@typescript-eslint/indent":
- error
- 4
linebreak-style:
- error
- unix
quotes:
- error
- double
- allowTemplateLiterals: true
avoidEscape: true
semi:
- error
- always
"@typescript-eslint/no-explicit-any":
- off
"@typescript-eslint/no-unused-vars":
- error
- argsIgnorePattern: _.*
no-unused-vars:
- error
no-trailing-spaces:
- error
key-spacing:
- error
space-before-blocks:
- error
block-spacing:
- error
brace-style:
- error
curly:
- error
keyword-spacing:
- error
"@typescript-eslint/no-var-requires":
- off
no-multiple-empty-lines:
- error
- max: 1
func-call-spacing:
- error
- never
padding-line-between-statements:
- error
- blankLine: always
prev: function
next: function
no-var:
- error
no-unused-expressions:
- error
prefer-const:
- error
prefer-template:
- error
template-curly-spacing:
- error

2914
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,14 @@
"postinstall": "cd src/svelte-components && npm i"
},
"dependencies": {
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"browserify": "^17.0.0",
"eslint": "^8.23.0",
"eslint-config-standard-with-typescript": "^22.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.5",
"eslint-plugin-promise": "^6.0.1",
"jshint": "^2.13.4",
"jstransformer-escape-html": "^1.1.0",
"jstransformer-scss": "^2.0.0",
@@ -16,4 +23,4 @@
"lodash.merge": "4.6.2",
"pug-cli": "^1.0.0-alpha6"
}
}
}

View File

@@ -1,143 +1,123 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
const api = require("./api");
const merge = require("lodash.merge");
const config_defaults = require("../resources/onefinity_defaults.json");
const variant_defaults = {
machinist_x35: require("../resources/onefinity_machinist_x35_defaults.json"),
woodworker_x35: require("../resources/onefinity_woodworker_x35_defaults.json"),
woodworker_x50: require("../resources/onefinity_woodworker_x50_defaults.json"),
journeyman_x50: require("../resources/onefinity_journeyman_x50_defaults.json")
machinist_x35: require("../resources/onefinity_machinist_x35_defaults.json"),
woodworker_x35: require("../resources/onefinity_woodworker_x35_defaults.json"),
woodworker_x50: require("../resources/onefinity_woodworker_x50_defaults.json"),
journeyman_x50: require("../resources/onefinity_journeyman_x50_defaults.json")
};
const api = require('./api');
module.exports = {
template: '#admin-general-view-template',
props: ['config', 'state'],
template: "#admin-general-view-template",
props: ["config", "state"],
data: function () {
return {
confirmReset: false,
autoCheckUpgrade: true,
reset_variant: ''
}
},
ready: function () {
this.autoCheckUpgrade = this.config.admin['auto-check-upgrade']
},
methods: {
backup: function () {
document.getElementById('download-target').src = '/api/config/download';
data: function () {
return {
confirmReset: false,
autoCheckUpgrade: true,
reset_variant: ""
};
},
restore_config: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$('.restore-config')[0].reset();
$('.restore-config input').click();
ready: function () {
this.autoCheckUpgrade = this.config.admin["auto-check-upgrade"];
},
restore: function (e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
methods: {
backup: function () {
document.getElementById("download-target").src = "/api/config/download";
},
const fileReader = new FileReader();
fileReader.onload = async ({ target }) => {
let config;
try {
config = JSON.parse(target.result);
} catch (ex) {
api.alert("Invalid config file");
return;
restore_config: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$(".restore-config")[0].reset();
$(".restore-config input").click();
},
restore: function (e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
const fileReader = new FileReader();
fileReader.onload = async ({ target }) => {
let config;
try {
config = JSON.parse(target.result);
} catch (ex) {
api.alert("Invalid config file");
return;
}
try {
await api.put("config/save", config);
this.$dispatch("update");
SvelteComponents.showDialog("Message", {
title: "Success",
message: "Configuration restored"
});
} catch (error) {
api.alert("Restore failed", error);
}
};
fileReader.readAsText(files[0]);
},
reset: async function () {
const config = merge(
{},
config_defaults,
variant_defaults[this.reset_variant]
);
try {
await api.put("config/save", config);
this.confirmReset = false;
this.$dispatch("update");
SvelteComponents.showDialog("Message", {
title: "Success",
message: "Configuration restored"
});
} catch (err) {
api.alert("Restore failed");
console.error("Restore failed", err);
}
},
check: function () {
this.$dispatch("check");
},
upgrade: function () {
this.$dispatch("upgrade");
},
upload_firmware: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$(".upload-firmware")[0].reset();
$(".upload-firmware input").click();
},
upload: function (e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
this.$dispatch("upload", files[0]);
},
change_auto_check_upgrade: function () {
this.config.admin["auto-check-upgrade"] = this.autoCheckUpgrade;
this.$dispatch("config-changed");
}
try {
await api.put('config/save', config);
this.$dispatch('update');
SvelteComponents.showDialog("Message", { title: "Success", message: "Configuration restored" })
} catch (error) {
api.alert('Restore failed', error);
}
}
fileReader.readAsText(files[0]);
},
reset: async function () {
const config = merge(
{},
config_defaults,
variant_defaults[this.reset_variant]
);
try {
await api.put('config/save', config)
this.confirmReset = false;
this.$dispatch('update');
SvelteComponents.showDialog("Message", { title: "Success", message: "Configuration restored" })
} catch (err) {
api.alert('Restore failed');
console.error('Restore failed', err);
}
},
check: function () {
this.$dispatch('check')
},
upgrade: function () {
this.$dispatch('upgrade')
},
upload_firmware: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$('.upload-firmware')[0].reset();
$('.upload-firmware input').click();
},
upload: function (e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
this.$dispatch('upload', files[0]);
},
change_auto_check_upgrade: function () {
this.config.admin['auto-check-upgrade'] = this.autoCheckUpgrade;
this.$dispatch('config-changed');
}
}
}
};

View File

@@ -1,14 +1,14 @@
module.exports = {
template: "#admin-network-view-template",
template: "#admin-network-view-template",
attached: function () {
this.svelteComponent = SvelteComponents.createComponent(
"AdminNetworkView",
document.getElementById("admin-network")
);
},
attached: function () {
this.svelteComponent = SvelteComponents.createComponent(
"AdminNetworkView",
document.getElementById("admin-network")
);
},
detached: function() {
this.svelteComponent.$destroy();
}
detached: function() {
this.svelteComponent.$destroy();
}
};

View File

@@ -1,77 +1,37 @@
'use strict'
"use strict";
async function callApi(method, url, body) {
try {
const response = await fetch(`/api/${url}`, {
method,
headers: {
"Content-Type": "application/json"
},
body
});
function api_cb(method, url, data, config) {
config = $.extend({
type: method,
url: '/api/' + url,
dataType: 'json',
cache: false
}, config);
if (response.ok) {
return response.json();
}
if (typeof data == 'object') {
config.data = JSON.stringify(data);
config.contentType = 'application/json; charset=utf-8';
}
throw new Error(await response.text());
} catch (error) {
console.debug(`API Error: ${url}: ${error}`);
var d = $.Deferred();
$.ajax(config).success(function (data, status, xhr) {
d.resolve(data, status, xhr);
}).error(function (xhr, status, error) {
var text = xhr.responseText;
try {text = $.parseJSON(xhr.responseText)} catch(e) {}
if (!text) text = error;
d.reject(text, xhr, status, error);
console.debug('API Error: ' + url + ': ' + text);
});
return d.promise();
throw error;
}
}
module.exports = {
get: function (url, config) {
return api_cb('GET', url, undefined, config);
},
get: function (url) {
return callApi("GET", url);
},
put: function(url, body = undefined) {
return callApi("PUT", url, body);
},
put: function(url, data, config) {
return api_cb('PUT', url, data, config);
},
post: function(url, data, config) {
return api_cb('POST', url, data, config);
},
upload: function(url, data, config) {
config = $.extend({
processData: false,
contentType: false,
cache: false,
data: data
}, config);
return api_cb('PUT', url, undefined, config);
},
'delete': function (url, config) {
return api_cb('DELETE', url, undefined, config);
},
alert: function (msg, error) {
if (typeof error != 'undefined') {
if (typeof error.message != 'undefined')
msg += '\n' + error.message;
else msg += '\n' + JSON.stringify(error);
delete: function (url) {
return callApi("DELETE", url);
}
alert(msg);
}
}
};

View File

@@ -5,411 +5,437 @@ const cookie = require("./cookie")("bbctrl-");
const Sock = require("./sock");
SvelteComponents.createComponent("DialogHost",
document.getElementById("svelte-dialog-host")
document.getElementById("svelte-dialog-host")
);
function is_newer_version(current, latest) {
const pattern = /(\d+)\.(\d+)\.(\d+)(.*)/;
const currentParts = current.match(pattern);
const latestParts = latest.match(pattern);
const pattern = /(\d+)\.(\d+)\.(\d+)(.*)/;
const currentParts = current.match(pattern);
const latestParts = latest.match(pattern);
if (!currentParts || !latestParts) {
return false;
}
if (!currentParts || !latestParts) {
return false;
}
// Normal version comparisons
const major = latestParts[1] - currentParts[1];
const minor = latestParts[2] - currentParts[2];
const patch = latestParts[3] - currentParts[3];
// Normal version comparisons
const major = latestParts[1] - currentParts[1];
const minor = latestParts[2] - currentParts[2];
const patch = latestParts[3] - currentParts[3];
// If current is a pre-release, and latest is a release
const betaToRelease =
latestParts[4].length === 0 && currentParts[4].length > 0;
// If current is a pre-release, and latest is a release
const betaToRelease = latestParts[4].length === 0 && currentParts[4].length > 0;
switch (true) {
case major > 0:
case major === 0 && minor > 0:
case major === 0 && minor === 0 && patch > 0:
case major === 0 && minor === 0 && patch === 0 && betaToRelease:
return true;
switch (true) {
case major > 0:
case major === 0 && minor > 0:
case major === 0 && minor === 0 && patch > 0:
case major === 0 && minor === 0 && patch === 0 && betaToRelease:
return true;
default:
return false;
}
default:
return false;
}
}
function is_object(o) {
return o !== null && typeof o == "object";
return o !== null && typeof o == "object";
}
function is_array(o) {
return Array.isArray(o);
return Array.isArray(o);
}
function update_array(dst, src) {
while (dst.length) dst.pop();
for (var i = 0; i < src.length; i++) Vue.set(dst, i, src[i]);
while (dst.length) {
dst.pop();
}
for (let i = 0; i < src.length; i++) {
Vue.set(dst, i, src[i]);
}
}
function hasOwnProperty(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function update_object(dst, src, remove) {
var props, index, key, value;
let props, index, key, value;
if (remove) {
props = Object.getOwnPropertyNames(dst);
if (remove) {
props = Object.getOwnPropertyNames(dst);
for (index in props) {
key = props[index];
if (!src.hasOwnProperty(key)) Vue.delete(dst, key);
for (index in props) {
key = props[index];
if (!hasOwnProperty(src, key)) {
Vue.delete(dst, key);
}
}
}
}
props = Object.getOwnPropertyNames(src);
for (index in props) {
key = props[index];
value = src[key];
props = Object.getOwnPropertyNames(src);
for (index in props) {
key = props[index];
value = src[key];
if (is_array(value) && dst.hasOwnProperty(key) && is_array(dst[key]))
update_array(dst[key], value);
else if (is_object(value) && dst.hasOwnProperty(key) && is_object(dst[key]))
update_object(dst[key], value, remove);
else Vue.set(dst, key, value);
}
if (is_array(value) && hasOwnProperty(dst, key) && is_array(dst[key])) {
update_array(dst[key], value);
} else if (is_object(value) && hasOwnProperty(dst, key) && is_object(dst[key])) {
update_object(dst[key], value, remove);
} else {
Vue.set(dst, key, value);
}
}
}
module.exports = new Vue({
el: "body",
el: "body",
data: function () {
return {
status: "connecting",
currentView: "loading",
display_units: localStorage.getItem("display_units") || "METRIC",
index: -1,
modified: false,
template: require("../resources/config-template.json"),
config: {
settings: { units: "METRIC" },
motors: [{}, {}, {}, {}],
version: "<loading>",
full_version: "<loading>",
},
state: {
messages: [],
},
video_size: cookie.get("video-size", "small"),
crosshair: cookie.get("crosshair", "false") != "false",
errorTimeout: 30,
errorTimeoutStart: 0,
errorShow: false,
errorMessage: "",
confirmUpgrade: false,
confirmUpload: false,
firmwareUpgrading: false,
checkedUpgrade: false,
firmwareName: "",
latestVersion: ""
};
},
components: {
estop: { template: "#estop-template" },
"loading-view": { template: "<h1>Loading...</h1>" },
"control-view": require("./control-view"),
"settings-view": require("./settings-view"),
"motor-view": require("./motor-view"),
"tool-view": require("./tool-view"),
"io-view": require("./io-view"),
"admin-general-view": require("./admin-general-view"),
"admin-network-view": require("./admin-network-view"),
"help-view": { template: "#help-view-template" },
"cheat-sheet-view": {
template: "#cheat-sheet-view-template",
data: function () {
data: function () {
return {
showUnimplemented: false
status: "connecting",
currentView: "loading",
display_units: localStorage.getItem("display_units") || "METRIC",
index: -1,
modified: false,
template: require("../resources/config-template.json"),
config: {
settings: { units: "METRIC" },
motors: [{}, {}, {}, {}],
version: "<loading>",
full_version: "<loading>",
},
state: {
messages: [],
},
video_size: cookie.get("video-size", "small"),
crosshair: cookie.get("crosshair", "false") != "false",
errorTimeout: 30,
errorTimeoutStart: 0,
errorShow: false,
errorMessage: "",
confirmUpgrade: false,
confirmUpload: false,
firmwareUpgrading: false,
checkedUpgrade: false,
firmwareName: "",
latestVersion: ""
};
},
},
},
watch: {
display_units: function (value) {
localStorage.setItem("display_units", value);
SvelteComponents.setDisplayUnits(value);
},
},
events: {
"config-changed": function () {
this.modified = true;
},
send: function (msg) {
if (this.status == "connected") {
this.sock.send(msg);
}
components: {
estop: { template: "#estop-template" },
"loading-view": { template: "<h1>Loading...</h1>" },
"control-view": require("./control-view"),
"settings-view": require("./settings-view"),
"motor-view": require("./motor-view"),
"tool-view": require("./tool-view"),
"io-view": require("./io-view"),
"admin-general-view": require("./admin-general-view"),
"admin-network-view": require("./admin-network-view"),
"help-view": { template: "#help-view-template" },
"cheat-sheet-view": {
template: "#cheat-sheet-view-template",
data: function () {
return {
showUnimplemented: false
};
},
},
},
connected: function () {
this.update();
watch: {
display_units: function (value) {
localStorage.setItem("display_units", value);
SvelteComponents.setDisplayUnits(value);
},
},
update: function () {
this.update();
},
events: {
"config-changed": function () {
this.modified = true;
},
check: async function () {
try {
const response = await fetch("https://raw.githubusercontent.com/OneFinityCNC/onefinity-release/master/latest.txt", {
cache: "no-cache"
});
send: function (msg) {
if (this.status == "connected") {
this.sock.send(msg);
}
},
this.latestVersion = (await response.text()).trim();
} catch (err) {
this.latestVersion = "";
}
},
connected: function () {
this.update();
},
upgrade: function () {
this.confirmUpgrade = true;
},
update: function () {
this.update();
},
upload: function (firmware) {
this.firmware = firmware;
this.firmwareName = firmware.name;
this.confirmUpload = true;
},
check: async function () {
try {
const response = await fetch("https://raw.githubusercontent.com/OneFinityCNC/onefinity-release/master/latest.txt", {
cache: "no-cache"
});
error: function (msg) {
// Honor user error blocking
if (Date.now() - this.errorTimeoutStart < this.errorTimeout * 1000)
return;
this.latestVersion = (await response.text()).trim();
} catch (err) {
this.latestVersion = "";
}
},
// Wait at least 1 sec to pop up repeated errors
if (1 < msg.repeat && Date.now() - msg.ts < 1000) {
return;
}
upgrade: function () {
this.confirmUpgrade = true;
},
// Popup error dialog
this.errorShow = true;
this.errorMessage = msg.msg;
},
},
upload: function (firmware) {
this.firmware = firmware;
this.firmwareName = firmware.name;
this.confirmUpload = true;
},
computed: {
popupMessages: function () {
const msgs = [];
for (let i = 0; i < this.state.messages.length; i++) {
const text = this.state.messages[i].text;
if (!/^#/.test(text)) {
msgs.push(text);
}
}
return msgs;
},
},
ready: function () {
$(window).on("hashchange", this.parse_hash);
this.connect();
SvelteComponents.registerControllerMethods({
dispatch: (...args) => this.$dispatch(...args)
});
},
methods: {
block_error_dialog: function () {
this.errorTimeoutStart = Date.now();
this.errorShow = false;
},
toggle_video: function (e) {
if (this.video_size == "small") this.video_size = "large";
else if (this.video_size == "large") this.video_size = "small";
cookie.set("video-size", this.video_size);
},
toggle_crosshair: function (e) {
e.preventDefault();
this.crosshair = !this.crosshair;
cookie.set("crosshair", this.crosshair);
},
estop: function () {
if (this.state.xx == "ESTOPPED") api.put("clear");
else api.put("estop");
},
upgrade_confirmed: async function () {
this.confirmUpgrade = false;
try {
await api.put("upgrade");
this.firmwareUpgrading = true;
} catch (err) {
api.alert("Error during upgrade.");
console.error("Error during upgrade", err);
}
},
upload_confirmed: function () {
this.confirmUpload = false;
const form = new FormData();
form.append("firmware", this.firmware);
$.ajax({
url: "/api/firmware/update",
type: "PUT",
data: form,
cache: false,
contentType: false,
processData: false,
})
.success(
function () {
this.firmwareUpgrading = true;
}.bind(this)
)
.error(
function (err) {
api.alert("Firmware update failed");
console.error("Firmware update failed", err);
}.bind(this)
);
},
show_upgrade: function () {
if (!this.latestVersion) {
return false;
}
return is_newer_version(this.config.version, this.latestVersion);
},
showShutdownDialog: function () {
SvelteComponents.showDialog("Shutdown");
},
update: async function () {
const config = await api.get("config/load");
update_object(this.config, config, true);
this.parse_hash();
if (!this.checkedUpgrade) {
this.checkedUpgrade = true;
var check = this.config.admin["auto-check-upgrade"];
if (typeof check == "undefined" || check) this.$emit("check");
}
SvelteComponents.handleConfigUpdate(this.config);
},
connect: function () {
this.sock = new Sock(`//${location.host}/sockjs`);
this.sock.onmessage = (e) => {
if (typeof e.data != "object") {
return;
}
if ("log" in e.data) {
if (e.data.log.msg !== "Switch not found") {
this.$broadcast("log", e.data.log);
}
}
// Check for session ID change on controller
if ("sid" in e.data) {
if (typeof this.sid == "undefined") {
this.sid = e.data.sid;
} else if (this.sid != e.data.sid) {
if (
typeof this.hostname !== "undefined" &&
location.hostname !== "localhost"
) {
location.hostname = this.hostname;
error: function (msg) {
// Honor user error blocking
if (Date.now() - this.errorTimeoutStart < this.errorTimeout * 1000) {
return;
}
location.reload();
}
}
// Wait at least 1 sec to pop up repeated errors
if (1 < msg.repeat && Date.now() - msg.ts < 1000) {
return;
}
update_object(this.state, e.data, false);
SvelteComponents.handleControllerStateUpdate(this.state);
delete this.state.log;
this.$broadcast("update");
};
this.sock.onopen = () => {
this.status = "connected";
this.$emit(this.status);
this.$broadcast(this.status);
};
this.sock.onclose = () => {
this.status = "disconnected";
this.$emit(this.status);
this.$broadcast(this.status);
};
// Popup error dialog
this.errorShow = true;
this.errorMessage = msg.msg;
},
},
parse_hash: function () {
var hash = location.hash.substr(1);
computed: {
popupMessages: function () {
const msgs = [];
if (!hash.trim().length) {
location.hash = "control";
return;
}
for (let i = 0; i < this.state.messages.length; i++) {
const text = this.state.messages[i].text;
if (!/^#/.test(text)) {
msgs.push(text);
}
}
var parts = hash.split(":");
if (parts.length == 2) this.index = parts[1];
this.currentView = parts[0];
return msgs;
},
},
save: async function () {
const selected_tool = this.config.tool["selected-tool"];
const saveModbus =
selected_tool !== "pwm" &&
selected_tool !== "laser" &&
selected_tool !== "router";
const settings = {
["tool"]: { ...this.config.tool },
["pwm-spindle"]: { ...this.config["pwm-spindle"] },
["modbus-spindle"]: saveModbus
? { ...this.config["modbus-spindle"] }
: undefined,
};
delete settings.tool["tool-type"];
ready: function () {
$(window).on("hashchange", this.parse_hash);
this.connect();
this.config["selected-tool-settings"][selected_tool] = settings;
try {
await api.put("config/save", this.config);
this.modified = false;
} catch (error) {
api.alert("Save failed", error);
}
SvelteComponents.registerControllerMethods({
dispatch: (...args) => this.$dispatch(...args)
});
},
close_messages: function (action) {
if (action == "stop") api.put("stop");
if (action == "continue") api.put("unpause");
methods: {
block_error_dialog: function () {
this.errorTimeoutStart = Date.now();
this.errorShow = false;
},
// Acknowledge messages
if (this.state.messages.length) {
var id = this.state.messages.slice(-1)[0].id;
api.put("message/" + id + "/ack");
}
toggle_video: function () {
if (this.video_size == "small") {
this.video_size = "large";
} else if (this.video_size == "large") {
this.video_size = "small";
}
cookie.set("video-size", this.video_size);
},
toggle_crosshair: function (e) {
e.preventDefault();
this.crosshair = !this.crosshair;
cookie.set("crosshair", this.crosshair);
},
estop: function () {
if (this.state.xx == "ESTOPPED") {
api.put("clear");
} else {
api.put("estop");
}
},
upgrade_confirmed: async function () {
this.confirmUpgrade = false;
try {
await api.put("upgrade");
this.firmwareUpgrading = true;
} catch (err) {
api.alert("Error during upgrade.");
console.error("Error during upgrade", err);
}
},
upload_confirmed: function () {
this.confirmUpload = false;
const form = new FormData();
form.append("firmware", this.firmware);
$.ajax({
url: "/api/firmware/update",
type: "PUT",
data: form,
cache: false,
contentType: false,
processData: false,
})
.success(
function () {
this.firmwareUpgrading = true;
}.bind(this)
)
.error(
function (err) {
api.alert("Firmware update failed");
console.error("Firmware update failed", err);
}.bind(this)
);
},
show_upgrade: function () {
if (!this.latestVersion) {
return false;
}
return is_newer_version(this.config.version, this.latestVersion);
},
showShutdownDialog: function () {
SvelteComponents.showDialog("Shutdown");
},
update: async function () {
const config = await api.get("config/load");
update_object(this.config, config, true);
this.parse_hash();
if (!this.checkedUpgrade) {
this.checkedUpgrade = true;
const check = this.config.admin["auto-check-upgrade"];
if (typeof check == "undefined" || check) {
this.$emit("check");
}
}
SvelteComponents.handleConfigUpdate(this.config);
},
connect: function () {
this.sock = new Sock(`//${location.host}/sockjs`);
this.sock.onmessage = (e) => {
if (typeof e.data != "object") {
return;
}
if ("log" in e.data) {
if (e.data.log.msg !== "Switch not found") {
this.$broadcast("log", e.data.log);
}
}
// Check for session ID change on controller
if ("sid" in e.data) {
if (typeof this.sid == "undefined") {
this.sid = e.data.sid;
} else if (this.sid != e.data.sid) {
if (this.hostname && location.hostname !== "localhost") {
location.hostname = this.hostname;
}
location.reload();
}
}
update_object(this.state, e.data, false);
SvelteComponents.handleControllerStateUpdate(this.state);
delete this.state.log;
this.$broadcast("update");
};
this.sock.onopen = () => {
this.status = "connected";
this.$emit(this.status);
this.$broadcast(this.status);
};
this.sock.onclose = () => {
this.status = "disconnected";
this.$emit(this.status);
this.$broadcast(this.status);
};
},
parse_hash: function () {
const hash = location.hash.substr(1);
if (!hash.trim().length) {
location.hash = "control";
return;
}
const parts = hash.split(":");
if (parts.length == 2) {
this.index = parts[1];
}
this.currentView = parts[0];
},
save: async function () {
const selected_tool = this.config.tool["selected-tool"];
const saveModbus =
selected_tool !== "pwm" &&
selected_tool !== "laser" &&
selected_tool !== "router";
const settings = {
["tool"]: { ...this.config.tool },
["pwm-spindle"]: { ...this.config["pwm-spindle"] },
["modbus-spindle"]: saveModbus
? { ...this.config["modbus-spindle"] }
: undefined,
};
delete settings.tool["tool-type"];
this.config["selected-tool-settings"][selected_tool] = settings;
try {
await api.put("config/save", this.config);
this.modified = false;
} catch (error) {
api.alert("Save failed", error);
}
},
close_messages: function (action) {
if (action == "stop") {
api.put("stop");
}
if (action == "continue") {
api.put("unpause");
}
// Acknowledge messages
if (this.state.messages.length) {
const id = this.state.messages.slice(-1)[0].id;
api.put("message/" + id + "/ack");
}
},
},
},
});

View File

@@ -1,65 +1,37 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
module.exports = {
template: '#axis-control-template',
props: ['axes', 'colors', 'enabled', 'adjust', 'step'],
template: "#axis-control-template",
props: ["axes", "colors", "enabled", "adjust", "step"],
methods: {
jog: function (axis, ring, direction) {
const value = direction * this.value(ring);
this.$dispatch(this.step ? "step" : "jog", this.axes[axis], value);
},
methods: {
jog: function (axis, ring, direction) {
var value = direction * this.value(ring);
this.$dispatch(this.step ? 'step' : 'jog', this.axes[axis], value);
},
back2zero: function(axis0,axis1) {
this.$dispatch("back2zero",this.axes[axis0],this.axes[axis1]);
},
back2zero: function(axis0,axis1) {
this.$dispatch('back2zero',this.axes[axis0],this.axes[axis1])
},
release: function (axis) {
if (!this.step) {
this.$dispatch("jog", this.axes[axis], 0);
}
},
value: function (ring) {
const adjust = [0.01, 0.1, 1][this.adjust];
if (this.step) {
return adjust * [0.1, 1, 10, 100][ring];
}
return adjust * [0.1, 0.25, 0.5, 1][ring];
},
release: function (axis) {
if (!this.step) this.$dispatch('jog', this.axes[axis], 0)
},
value: function (ring) {
var adjust = [0.01, 0.1, 1][this.adjust];
if (this.step) return adjust * [0.1, 1, 10, 100][ring];
return adjust * [0.1, 0.25, 0.5, 1][ring];
},
text: function (ring) {
var value = this.value(ring) * (this.step ? 1 : 100);
value = parseFloat(value.toFixed(3));
return value + (this.step ? '' : '%');
text: function (ring) {
let value = this.value(ring) * (this.step ? 1 : 100);
value = parseFloat(value.toFixed(3));
return value + (this.step ? "" : "%");
}
}
}
}
};

View File

@@ -1,194 +1,254 @@
'use strict'
"use strict";
module.exports = {
props: ['state', 'config'],
props: ["state", "config"],
computed: {
metric: function () { this.$root.display_units === "METRIC" },
x: function () { return this._compute_axis('x') },
y: function () { return this._compute_axis('y') },
z: function () { return this._compute_axis('z') },
a: function () { return this._compute_axis('a') },
b: function () { return this._compute_axis('b') },
c: function () { return this._compute_axis('c') },
axes: function () { return this._compute_axes() }
},
computed: {
metric: function () {
return this.$root.display_units === "METRIC";
},
methods: {
_convert_length: function (value) {
return this.metric
? value
: value / 25.4;
},
x: function () {
return this._compute_axis("x");
},
_length_str: function (value) {
return this._convert_length(value).toLocaleString() +
(this.metric ? ' mm' : ' in');
},
y: function () {
return this._compute_axis("y");
},
_compute_axis: function (axis) {
var abs = this.state[axis + 'p'] || 0;
var off = this.state['offset_' + axis];
var motor_id = this._get_motor_id(axis);
var motor = motor_id == -1 ? {} : this.config.motors[motor_id];
var enabled = typeof motor.enabled != 'undefined' && motor.enabled;
var homingMode = motor['homing-mode']
var homed = this.state[motor_id + 'homed'];
var min = this.state[motor_id + 'tn'];
var max = this.state[motor_id + 'tm'];
var dim = max - min;
var pathMin = this.state['path_min_' + axis];
var pathMax = this.state['path_max_' + axis];
var pathDim = pathMax - pathMin;
var under = pathMin + off < min;
var over = max < pathMax + off;
var klass = (homed ? 'homed' : 'unhomed') + ' axis-' + axis;
var state = 'UNHOMED';
var icon = 'question-circle';
var fault = this.state[motor_id + 'df'] & 0x1f;
var shutdown = this.state.power_shutdown;
var title;
var ticon = 'question-circle';
var tstate = 'NO FILE';
var toolmsg;
var tklass = (homed ? 'homed' : 'unhomed') + ' axis-' + axis;
z: function () {
return this._compute_axis("z");
},
if (fault || shutdown) {
state = shutdown ? 'SHUTDOWN' : 'FAULT';
klass += ' error';
icon = 'exclamation-circle';
} else if (homed) {
state = 'HOMED';
icon = 'check-circle';
}
a: function () {
return this._compute_axis("a");
},
if (0 < dim && dim < pathDim) {
tstate = 'NO FIT';
tklass += ' error';
ticon = 'ban';
} else {
if (over || under) {
tstate = over ? 'OVER' : 'UNDER';
tklass += ' warn';
ticon = 'exclamation-circle';
} else {
tstate = 'OK';
ticon = 'check-circle';
b: function () {
return this._compute_axis("b");
},
c: function () {
return this._compute_axis("c");
},
axes: function () {
return this._compute_axes();
}
}
switch (state) {
case 'UNHOMED': title = 'Click the home button to home axis.'; break;
case 'HOMED': title = 'Axis successfuly homed.'; break;
case 'FAULT':
title = 'Motor driver fault. A potentially damaging electrical ' +
'condition was detected and the motor driver was shutdown. ' +
'Please power down the controller and check your motor cabling. ' +
'See the "Motor Faults" table on the "Indicators" tab for more ' +
'information.';
break;
case 'SHUTDOWN':
title = 'Motor power fault. All motors in shutdown. ' +
'See the "Power Faults" table on the "Indicators" tab for more ' +
'information. Reboot controller to reset.';
}
switch (tstate) {
case 'OVER':
toolmsg = 'Caution: The current tool path file would move ' +
this._length_str(pathMax + off - max) + ' above axis limit with the current offset.';
break;
case 'UNDER':
toolmsg = 'Caution: The current tool path file would move ' +
this._length_str(min - pathMin - off) + ' below limit with the current offset.';
break;
case 'NO FIT':
toolmsg = 'Warning: The current tool path dimensions (' +
this._length_str(pathDim) + ') exceed axis dimensions (' +
this._length_str(dim) + ') by ' +
this._length_str(pathDim - dim) + '.';
break;
default:
toolmsg = 'Tool path ' + axis + ' dimensions OK.';
break;
}
return {
pos: abs - off,
abs: abs,
off: off,
min: min,
max: max,
dim: dim,
pathMin: pathMin,
pathMax: pathMax,
pathDim: pathDim,
motor: motor_id,
enabled: enabled,
homingMode: homingMode,
homed: homed,
klass: klass,
state: state,
icon: icon,
title: title,
ticon: ticon,
tstate: tstate,
toolmsg: toolmsg,
tklass: tklass
}
},
_get_motor_id: function (axis) {
for (var i = 0; i < this.config.motors.length; i++) {
var motor = this.config.motors[i];
if (motor.axis.toLowerCase() == axis) return i;
}
methods: {
_convert_length: function (value) {
return this.metric
? value
: value / 25.4;
},
return -1;
},
_length_str: function (value) {
return this._convert_length(value).toLocaleString() + (this.metric ? " mm" : " in");
},
_compute_axes: function () {
var homed = false;
_compute_axis: function (axis) {
const abs = this.state[`${axis}p`] || 0;
const off = this.state[`offset_${axis}`];
const motor_id = this._get_motor_id(axis);
const motor = motor_id == -1 ? {} : this.config.motors[motor_id];
const enabled = typeof motor.enabled != "undefined" && motor.enabled;
const homingMode = motor["homing-mode"];
const homed = this.state[`${motor_id}homed`];
const min = this.state[`${motor_id}tn`];
const max = this.state[`${motor_id}tm`];
const dim = max - min;
const pathMin = this.state[`path_min_${axis}`];
const pathMax = this.state[`path_max_${axis}`];
const pathDim = pathMax - pathMin;
const under = pathMin + off < min;
const over = max < pathMax + off;
let klass = `${homed ? "homed" : "unhomed"} axis-${axis}`;
let state = "UNHOMED";
let icon = "question-circle";
const fault = this.state[`${motor_id}df`] & 0x1f;
const shutdown = this.state.power_shutdown;
let title;
let ticon = "question-circle";
let tstate = "NO FILE";
let toolmsg;
let tklass = `${homed ? "homed" : "unhomed"} axis-${axis}`;
for (var name of 'xyzabc') {
var axis = this[name];
if (fault || shutdown) {
state = shutdown ? "SHUTDOWN" : "FAULT";
klass += " error";
icon = "exclamation-circle";
} else if (homed) {
state = "HOMED";
icon = "check-circle";
}
if (!axis.enabled) continue
if (!axis.homed) { homed = false; break }
homed = true;
}
if (0 < dim && dim < pathDim) {
tstate = "NO FIT";
tklass += " error";
ticon = "ban";
} else {
if (over || under) {
tstate = over ? "OVER" : "UNDER";
tklass += " warn";
ticon = "exclamation-circle";
} else {
tstate = "OK";
ticon = "check-circle";
}
}
var error = false;
var warn = false;
switch (state) {
case "UNHOMED":
title = "Click the home button to home axis.";
break;
if (homed)
for (name of 'xyzabc') {
axis = this[name];
case "HOMED":
title = "Axis successfuly homed.";
break;
if (!axis.enabled) continue;
if (axis.klass.indexOf('error') != -1) error = true;
if (axis.klass.indexOf('warn') != -1) warn = true;
case "FAULT":
title = [
`Motor driver fault. A potentially damaging electrical`,
`condition was detected and the motor driver was shutdown.`,
`Please power down the controller and check your motor cabling.`,
`See the "Motor Faults" table on the "Indicators" tab for more`,
`information.`,
].join(" ");
break;
case "SHUTDOWN":
title = [
`Motor power fault. All motors in shutdown.`,
`See the "Power Faults" table on the "Indicators" tab for more`,
`information. Reboot controller to reset.`
].join(" ");
break;
}
switch (tstate) {
case "OVER":
toolmsg = [
`Caution: The current tool path file would move`,
`${this._length_str(pathMax + off - max)}`,
`above axis limit with the current offset.`
].join(" ");
break;
case "UNDER":
toolmsg = [
`Caution: The current tool path file would move`,
`${this._length_str(min - pathMin - off)}`,
`below limit with the current offset.`
].join(" ");
break;
case "NO FIT":
toolmsg = [
`Warning: The current tool path dimensions`,
`(${this._length_str(pathDim)}) exceed axis dimensions`,
`(${this._length_str(dim)}) by ${this._length_str(pathDim - dim)}.`
].join(" ");
break;
default:
toolmsg = `Tool path ${axis} dimensions OK.`;
break;
}
return {
pos: abs - off,
abs: abs,
off: off,
min: min,
max: max,
dim: dim,
pathMin: pathMin,
pathMax: pathMax,
pathDim: pathDim,
motor: motor_id,
enabled: enabled,
homingMode: homingMode,
homed: homed,
klass: klass,
state: state,
icon: icon,
title: title,
ticon: ticon,
tstate: tstate,
toolmsg: toolmsg,
tklass: tklass
};
},
_get_motor_id: function (axis) {
for (let i = 0; i < this.config.motors.length; i++) {
const motor = this.config.motors[i];
if (motor.axis.toLowerCase() == axis) {
return i;
}
}
return -1;
},
_compute_axes: function () {
let homed = false;
for (const name of "xyzabc") {
const axis = this[name];
if (!axis.enabled) {
continue;
}
if (!axis.homed) {
homed = false; break;
}
homed = true;
}
let error = false;
let warn = false;
if (homed) {
for (const name of "xyzabc") {
const axis = this[name];
if (!axis.enabled) {
continue;
}
if (axis.klass.indexOf("error") != -1) {
error = true;
}
if (axis.klass.indexOf("warn") != -1) {
warn = true;
}
}
}
let klass = homed ? "homed" : "unhomed";
if (error) {
klass += " error";
} else if (warn) {
klass += " warn";
}
if (!homed && this.ask_home) {
this.ask_home = false;
SvelteComponents.showDialog("HomeMachine", {
home: () => this.home()
});
}
return {
homed: homed,
klass: klass
};
}
var klass = homed ? 'homed' : 'unhomed';
if (error) klass += ' error';
else if (warn) klass += ' warn';
if (!homed && this.ask_home) {
this.ask_home = false;
SvelteComponents.showDialog("HomeMachine", {
home: () => this.home()
});
}
return {
homed: homed,
klass: klass
}
}
}
}
};

View File

@@ -1,87 +1,72 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
function _msg_equal(a, b) {
return a.level == b.level && a.source == b.source && a.where == b.where &&
a.msg == b.msg;
return a.level == b.level
&& a.source == b.source
&& a.where == b.where
&&a.msg == b.msg;
}
// Shared among all instances
var messages = [];
const messages = [];
module.exports = {
template: '#console-template',
template: "#console-template",
data: function () {
return {
messages
};
},
data: function () {
return {messages: messages}
},
events: {
log: function (msg) {
// There may be multiple instances of this module so ignore messages
// that have already been processed.
if (msg.logged) {
return;
}
msg.logged = true;
events: {
log: function (msg) {
// There may be multiple instances of this module so ignore messages
// that have already been processed.
if (msg.logged) return;
msg.logged = true;
// Make sure we have a message level
msg.level = msg.level || "info";
// Make sure we have a message level
msg.level = msg.level || 'info';
// Add to message log and count and collapse repeats
const repeat = messages.length && _msg_equal(msg, messages[0]);
if (repeat) {
messages[0].repeat++;
} else {
msg.repeat = msg.repeat || 1;
messages.unshift(msg);
while (256 < messages.length) {
messages.pop();
}
}
msg.ts = Date.now();
// Add to message log and count and collapse repeats
var repeat = messages.length && _msg_equal(msg, messages[0]);
if (repeat) messages[0].repeat++;
else {
msg.repeat = msg.repeat || 1;
messages.unshift(msg);
while (256 < messages.length) messages.pop();
}
msg.ts = Date.now();
// Write message to browser console for debugging
const text = JSON.stringify(msg);
if (msg.level == "error" || msg.level == "critical") {
console.error(text);
} else if (msg.level == "warning") {
console.warn(text);
} else if (msg.level == "debug" && console.debug) {
console.debug(text);
} else {
console.log(text);
}
// Write message to browser console for debugging
var text = JSON.stringify(msg);
if (msg.level == 'error' || msg.level == 'critical') console.error(text);
else if (msg.level == 'warning') console.warn(text);
else if (msg.level == 'debug' && console.debug) console.debug(text);
else console.log(text);
// Event on errors
if (msg.level == "error" || msg.level == "critical") {
this.$dispatch("error", msg);
}
}
},
// Event on errors
if (msg.level == 'error' || msg.level == 'critical')
this.$dispatch('error', msg);
methods: {
clear: function () {
messages.splice(0, messages.length);
},
}
},
methods: {
clear: function () {messages.splice(0, messages.length);},
}
}
};

View File

@@ -1,503 +1,501 @@
'use strict'
"use strict";
var api = require('./api');
var cookie = require('./cookie')('bbctrl-');
const api = require("./api");
const cookie = require("./cookie")("bbctrl-");
module.exports = {
template: '#control-view-template',
props: ['config', 'template', 'state'],
template: "#control-view-template",
props: ["config", "template", "state"],
data: function () {
return {
current_time: "",
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
mdi: '',
last_file: undefined,
last_file_time: undefined,
toolpath: {},
toolpath_progress: 0,
axes: 'xyzabc',
history: [],
speed_override: 1,
feed_override: 1,
jog_incr_amounts: {
"METRIC": {
fine: 0.1,
small: 1.0,
medium: 10,
large: 100,
data: function () {
return {
current_time: "",
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
mdi: "",
last_file: undefined,
last_file_time: undefined,
toolpath: {},
toolpath_progress: 0,
axes: "xyzabc",
history: [],
speed_override: 1,
feed_override: 1,
jog_incr_amounts: {
"METRIC": {
fine: 0.1,
small: 1.0,
medium: 10,
large: 100,
},
"IMPERIAL": {
fine: 0.005,
small: 0.05,
medium: 0.5,
large: 5,
}
},
jog_incr: localStorage.getItem("jog_incr") || "small",
jog_step: cookie.get_bool("jog-step"),
jog_adjust: parseInt(cookie.get("jog-adjust", 2)),
deleteGCode: false,
tab: "auto",
ask_home: true,
showGcodeMessage: false
};
},
components: {
"axis-control": require("./axis-control"),
"path-viewer": require("./path-viewer"),
"gcode-viewer": require("./gcode-viewer")
},
watch: {
jog_incr: function (value) {
localStorage.setItem("jog_incr", value);
},
"IMPERIAL": {
fine: 0.005,
small: 0.05,
medium: 0.5,
large: 5,
}
},
jog_incr: localStorage.getItem("jog_incr") || 'small',
jog_step: cookie.get_bool('jog-step'),
jog_adjust: parseInt(cookie.get('jog-adjust', 2)),
deleteGCode: false,
tab: 'auto',
ask_home: true,
showGcodeMessage: false
}
},
components: {
'axis-control': require('./axis-control'),
'path-viewer': require('./path-viewer'),
'gcode-viewer': require('./gcode-viewer')
},
"state.metric": {
handler: function (metric) {
this.mach_units = metric
? "METRIC"
: "IMPERIAL";
},
immediate: true
},
watch: {
jog_incr: function (value) {
localStorage.setItem("jog_incr", value);
},
'state.metric': {
handler: function (metric) {
this.mach_units = metric
? 'METRIC'
: 'IMPERIAL';
},
immediate: true
},
'state.line': function () {
if (this.mach_state != 'HOMING') {
this.$broadcast('gcode-line', this.state.line);
}
},
'state.selected_time': function () {
this.load();
},
jog_step: function () {
cookie.set_bool('jog-step', this.jog_step);
},
jog_adjust: function () {
cookie.set('jog-adjust', this.jog_adjust);
}
},
computed: {
display_units: {
cache: false,
get: function () {
return this.$root.display_units;
},
set: function (value) {
this.$root.display_units = value;
}
},
metric: function () {
return this.display_units === "METRIC";
},
mach_state: function () {
var cycle = this.state.cycle;
var state = this.state.xx;
if (typeof cycle != 'undefined' && state != 'ESTOPPED' &&
(cycle == 'jogging' || cycle == 'homing')) {
return cycle.toUpperCase();
}
return state || ''
},
pause_reason: function () {
return this.state.pr
},
is_running: function () {
return this.mach_state == 'RUNNING' || this.mach_state == 'HOMING';
},
is_stopping: function () {
return this.mach_state == 'STOPPING'
},
is_holding: function () {
return this.mach_state == 'HOLDING'
},
is_ready: function () {
return this.mach_state == 'READY'
},
is_idle: function () {
return this.state.cycle == 'idle'
},
is_paused: function () {
return this.is_holding &&
(this.pause_reason == 'User pause' ||
this.pause_reason == 'Program pause')
},
can_mdi: function () {
return this.is_idle || this.state.cycle == 'mdi'
},
can_set_axis: function () {
return this.is_idle
// TODO allow setting axis position during pause
return this.is_idle || this.is_paused
},
message: function () {
if (this.mach_state == 'ESTOPPED') {
return this.state.er;
}
if (this.mach_state == 'HOLDING') {
return this.state.pr;
}
if (this.state.messages.length) {
return this.state.messages.slice(-1)[0].text;
}
return '';
},
highlight_state: function () {
return this.mach_state == 'ESTOPPED' || this.mach_state == 'HOLDING';
},
plan_time: function () {
return this.state.plan_time
},
plan_time_remaining: function () {
if (!(this.is_stopping || this.is_running || this.is_holding)) {
return 0;
}
return this.toolpath.time - this.plan_time
},
eta: function () {
if (this.mach_state != 'RUNNING') {
return '';
}
var remaining = this.plan_time_remaining;
var d = new Date();
d.setSeconds(d.getSeconds() + remaining);
return d.toLocaleString();
},
progress: function () {
if (!this.toolpath.time || this.is_ready) {
return 0;
}
var p = this.plan_time / this.toolpath.time;
return p < 1 ? p : 1;
}
},
events: {
jog: function (axis, power) {
var data = { ts: new Date().getTime() };
data[axis] = power;
api.put('jog', data);
},
back2zero: function (axis0, axis1) {
this.send(`G0 ${axis0}0 ${axis1}0`);
},
step: function (axis, value) {
this.send(`
M70
G91
G0 ${axis}${value}
M72
`);
},
},
ready: function () {
this.load();
setInterval(() => {
this.current_time = new Date().toLocaleTimeString();
}, 1000);
SvelteComponents.registerControllerMethods({
stop: (...args) => this.stop(...args),
send: (...args) => this.send(...args),
isAxisHomed: (axis) => this[axis].homed,
unhome: (...args) => this.unhome(...args),
set_position: (...args) => this.set_position(...args),
set_home: (...args) => this.set_home(...args)
});
},
methods: {
getJogIncrStyle(value) {
const weight = `font-weight:${this.jog_incr === value ? 'bold' : 'normal'}`;
const color = this.jog_incr === value ? "color:#0078e7" : "";
return [weight, color].join(';');
},
jog_fn: function (x_jog, y_jog, z_jog, a_jog) {
const amount = this.jog_incr_amounts[this.display_units][this.jog_incr];
var xcmd = "X" + x_jog * amount;
var ycmd = "Y" + y_jog * amount;
var zcmd = "Z" + z_jog * amount;
var acmd = "A" + a_jog * amount;
this.send(`
G91
${this.metric ? "G21" : "G20"}
G0 ${xcmd}${ycmd}${zcmd}${acmd}
`);
},
send: function (msg) {
this.$dispatch('send', msg)
},
load: function () {
var file_time = this.state.selected_time;
var file = this.state.selected;
if (this.last_file == file && this.last_file_time == file_time) {
return;
}
this.last_file = file;
this.last_file_time = file_time;
this.$broadcast('gcode-load', file);
this.$broadcast('gcode-line', this.state.line);
this.toolpath_progress = 0;
this.load_toolpath(file, file_time);
},
load_toolpath: async function (file, file_time) {
this.toolpath = {};
if (!file || this.last_file_time != file_time) {
return;
}
this.showGcodeMessage = true;
while (this.showGcodeMessage) {
const toolpath = await api.get(`path/${file}`);
this.toolpath_progress = toolpath.progress;
if (toolpath.progress === 1 || typeof toolpath.progress == 'undefined') {
this.showGcodeMessage = false
if (toolpath.bounds) {
toolpath.filename = file;
this.toolpath_progress = 1;
this.toolpath = toolpath;
const state = this.$root.state;
for (let axis of 'xyzabc') {
Vue.set(state, 'path_min_' + axis, toolpath.bounds.min[axis]);
Vue.set(state, 'path_max_' + axis, toolpath.bounds.max[axis]);
"state.line": function () {
if (this.mach_state != "HOMING") {
this.$broadcast("gcode-line", this.state.line);
}
}
},
"state.selected_time": function () {
this.load();
},
jog_step: function () {
cookie.set_bool("jog-step", this.jog_step);
},
jog_adjust: function () {
cookie.set("jog-adjust", this.jog_adjust);
}
}
},
submit_mdi: function () {
this.send(this.mdi);
computed: {
display_units: {
cache: false,
get: function () {
return this.$root.display_units;
},
set: function (value) {
this.$root.display_units = value;
}
},
if (!this.history.length || this.history[0] != this.mdi) {
this.history.unshift(this.mdi);
}
metric: function () {
return this.display_units === "METRIC";
},
this.mdi = '';
},
mach_state: function () {
const cycle = this.state.cycle;
const state = this.state.xx;
mdi_start_pause: function () {
if (this.state.xx == 'RUNNING') {
this.pause();
} else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING') {
this.unpause();
} else {
this.submit_mdi();
}
},
if (state != "ESTOPPED" && (cycle == "jogging" || cycle == "homing")) {
return cycle.toUpperCase();
}
load_history: function (index) {
this.mdi = this.history[index];
},
return state || "";
},
open: function (e) {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$('.gcode-file-input')[0].reset();
$('.gcode-file-input input').click();
},
pause_reason: function () {
return this.state.pr;
},
upload: async function (e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
is_running: function () {
return this.mach_state == "RUNNING" || this.mach_state == "HOMING";
},
const file = files[0];
is_stopping: function () {
return this.mach_state == "STOPPING";
},
const extension = file.name.split(".").pop();
switch (extension.toLowerCase()) {
case "nc":
case "ngc":
case "gcode":
case "gc":
break;
is_holding: function () {
return this.mach_state == "HOLDING";
},
default:
alert(`Unsupported file type: ${extension}`);
return;
}
is_ready: function () {
return this.mach_state == "READY";
},
SvelteComponents.showDialog("Upload", {
file,
onComplete: () => {
this.last_file_time = undefined; // Force reload
this.$broadcast('gcode-reload', file.name);
is_idle: function () {
return this.state.cycle == "idle";
},
is_paused: function () {
return this.is_holding && (this.pause_reason == "User pause" || this.pause_reason == "Program pause");
},
can_mdi: function () {
return this.is_idle || this.state.cycle == "mdi";
},
can_set_axis: function () {
return this.is_idle;
// TODO allow setting axis position during pause
// return this.is_idle || this.is_paused;
},
message: function () {
if (this.mach_state == "ESTOPPED") {
return this.state.er;
}
if (this.mach_state == "HOLDING") {
return this.state.pr;
}
if (this.state.messages.length) {
return this.state.messages.slice(-1)[0].text;
}
return "";
},
highlight_state: function () {
return this.mach_state == "ESTOPPED" || this.mach_state == "HOLDING";
},
plan_time: function () {
return this.state.plan_time;
},
plan_time_remaining: function () {
if (!(this.is_stopping || this.is_running || this.is_holding)) {
return 0;
}
return this.toolpath.time - this.plan_time;
},
eta: function () {
if (this.mach_state != "RUNNING") {
return "";
}
const remaining = this.plan_time_remaining;
const d = new Date();
d.setSeconds(d.getSeconds() + remaining);
return d.toLocaleString();
},
progress: function () {
if (!this.toolpath.time || this.is_ready) {
return 0;
}
const p = this.plan_time / this.toolpath.time;
return Math.max(1, p);
}
});
},
delete_current: function () {
if (this.state.selected) {
api.delete('file/' + this.state.selected);
}
events: {
jog: function (axis, power) {
const data = { ts: new Date().getTime() };
data[axis] = power;
api.put("jog", data);
},
this.deleteGCode = false;
back2zero: function (axis0, axis1) {
this.send(`G0 ${axis0}0 ${axis1}0`);
},
step: function (axis, value) {
this.send(`
M70
G91
G0 ${axis}${value}
M72
`);
},
},
delete_all: function () {
api.delete('file');
this.deleteGCode = false;
ready: function () {
this.load();
setInterval(() => {
this.current_time = new Date().toLocaleTimeString();
}, 1000);
SvelteComponents.registerControllerMethods({
stop: (...args) => this.stop(...args),
send: (...args) => this.send(...args),
isAxisHomed: (axis) => this[axis].homed,
unhome: (...args) => this.unhome(...args),
set_position: (...args) => this.set_position(...args),
set_home: (...args) => this.set_home(...args)
});
},
home: function (axis) {
this.ask_home = false;
methods: {
getJogIncrStyle(value) {
const weight = `font-weight:${this.jog_incr === value ? "bold" : "normal"}`;
const color = this.jog_incr === value ? "color:#0078e7" : "";
if (typeof axis == 'undefined') {
api.put('home');
} else if (this[axis].homingMode != 'manual') {
api.put('home/' + axis);
} else {
SvelteComponents.showDialog("ManualHomeAxis", { axis });
}
},
return [weight, color].join(";");
},
set_home: function (axis, position) {
api.put('home/' + axis + '/set', { position: parseFloat(position) });
},
jog_fn: function (x_jog, y_jog, z_jog, a_jog) {
const amount = this.jog_incr_amounts[this.display_units][this.jog_incr];
unhome: function (axis) {
api.put('home/' + axis + '/clear');
},
const xcmd = `X${x_jog * amount}`;
const ycmd = `Y${y_jog * amount}`;
const zcmd = `Z${z_jog * amount}`;
const acmd = `A${a_jog * amount}`;
show_set_position: function (axis) {
SvelteComponents.showDialog("SetAxisPosition", { axis });
},
this.send(`
G91
${this.metric ? "G21" : "G20"}
G0 ${xcmd}${ycmd}${zcmd}${acmd}
`);
},
showMoveToZeroDialog: function (axes) {
SvelteComponents.showDialog("MoveToZero", { axes });
},
send: function (msg) {
this.$dispatch("send", msg);
},
showToolpathMessageDialog: function (axis) {
SvelteComponents.showDialog("Message", { title: this[axis].toolmsg });
},
load: function () {
const file_time = this.state.selected_time;
const file = this.state.selected;
if (this.last_file == file && this.last_file_time == file_time) {
return;
}
set_position: function (axis, position) {
api.put('position/' + axis, { 'position': parseFloat(position) });
},
this.last_file = file;
this.last_file_time = file_time;
zero_all: function () {
for (var axis of 'xyzabc') {
if (this[axis].enabled) {
this.zero(axis);
this.$broadcast("gcode-load", file);
this.$broadcast("gcode-line", this.state.line);
this.toolpath_progress = 0;
this.load_toolpath(file, file_time);
},
load_toolpath: async function (file, file_time) {
this.toolpath = {};
if (!file || this.last_file_time != file_time) {
return;
}
this.showGcodeMessage = true;
while (this.showGcodeMessage) {
const toolpath = await api.get(`path/${file}`);
this.toolpath_progress = toolpath.progress;
if (toolpath.progress === 1 || typeof toolpath.progress == "undefined") {
this.showGcodeMessage = false;
if (toolpath.bounds) {
toolpath.filename = file;
this.toolpath_progress = 1;
this.toolpath = toolpath;
const state = this.$root.state;
for (const axis of "xyzabc") {
Vue.set(state, `path_min_${axis}`, toolpath.bounds.min[axis]);
Vue.set(state, `path_max_${axis}`, toolpath.bounds.max[axis]);
}
}
}
}
},
submit_mdi: function () {
this.send(this.mdi);
if (!this.history.length || this.history[0] != this.mdi) {
this.history.unshift(this.mdi);
}
this.mdi = "";
},
mdi_start_pause: function () {
if (this.state.xx == "RUNNING") {
this.pause();
} else if (this.state.xx == "STOPPING" || this.state.xx == "HOLDING") {
this.unpause();
} else {
this.submit_mdi();
}
},
load_history: function (index) {
this.mdi = this.history[index];
},
open: function () {
// If we don't reset the form the browser may cache file if name is same
// even if contents have changed
$(".gcode-file-input")[0].reset();
$(".gcode-file-input input").click();
},
upload: async function (e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
const file = files[0];
const extension = file.name.split(".").pop();
switch (extension.toLowerCase()) {
case "nc":
case "ngc":
case "gcode":
case "gc":
break;
default:
alert(`Unsupported file type: ${extension}`);
return;
}
SvelteComponents.showDialog("Upload", {
file,
onComplete: () => {
this.last_file_time = undefined; // Force reload
this.$broadcast("gcode-reload", file.name);
}
});
},
delete_current: function () {
if (this.state.selected) {
api.delete(`file/${this.state.selected}`);
}
this.deleteGCode = false;
},
delete_all: function () {
api.delete("file");
this.deleteGCode = false;
},
home: function (axis) {
this.ask_home = false;
if (typeof axis == "undefined") {
api.put("home");
} else if (this[axis].homingMode != "manual") {
api.put(`home/${axis}`);
} else {
SvelteComponents.showDialog("ManualHomeAxis", { axis });
}
},
set_home: function (axis, position) {
api.put(`home/${axis}/set`, { position: parseFloat(position) });
},
unhome: function (axis) {
api.put(`home/${axis}/clear`);
},
show_set_position: function (axis) {
SvelteComponents.showDialog("SetAxisPosition", { axis });
},
showMoveToZeroDialog: function (axes) {
SvelteComponents.showDialog("MoveToZero", { axes });
},
showToolpathMessageDialog: function (axis) {
SvelteComponents.showDialog("Message", { title: this[axis].toolmsg });
},
set_position: function (axis, position) {
api.put(`position/${axis}`, { "position": parseFloat(position) });
},
zero_all: function () {
for (const axis of "xyzabc") {
if (this[axis].enabled) {
this.zero(axis);
}
}
},
zero: function (axis) {
if (typeof axis == "undefined") {
this.zero_all();
} else {
this.set_position(axis, 0);
}
},
start_pause: function () {
if (this.state.xx == "RUNNING") {
this.pause();
} else if (this.state.xx == "STOPPING" || this.state.xx == "HOLDING") {
this.unpause();
} else {
this.start();
}
},
start: function () {
api.put("start");
},
pause: function () {
api.put("pause");
},
unpause: function () {
api.put("unpause");
},
optional_pause: function () {
api.put("pause/optional");
},
stop: function () {
api.put("stop");
},
step: function () {
api.put("step");
},
override_feed: function () {
api.put(`override/feed/${this.feed_override}`);
},
override_speed: function () {
api.put(`override/speed/${this.speed_override}`);
},
current: function (axis, value) {
const x = value / 32.0;
if (this.state[`${axis}pl`] == x) {
return;
}
const data = {};
data[`${axis}pl`] = x;
this.send(JSON.stringify(data));
},
showProbeDialog: function (probeType) {
SvelteComponents.showDialog("Probe", { probeType });
}
}
},
zero: function (axis) {
if (typeof axis == 'undefined') {
this.zero_all();
} else {
this.set_position(axis, 0);
}
},
start_pause: function () {
if (this.state.xx == 'RUNNING') {
this.pause();
} else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING') {
this.unpause();
} else {
this.start();
}
},
start: function () {
api.put('start')
},
pause: function () {
api.put('pause')
},
unpause: function () {
api.put('unpause')
},
optional_pause: function () {
api.put('pause/optional')
},
stop: function () {
api.put('stop')
},
step: function () {
api.put('step')
},
override_feed: function () {
api.put('override/feed/' + this.feed_override)
},
override_speed: function () {
api.put('override/speed/' + this.speed_override)
},
current: function (axis, value) {
var x = value / 32.0;
if (this.state[axis + 'pl'] == x) {
return;
}
var data = {};
data[axis + 'pl'] = x;
this.send(JSON.stringify(data));
},
showProbeDialog: function (probeType) {
SvelteComponents.showDialog("Probe", { probeType });
}
},
mixins: [require('./axis-vars')]
}
mixins: [require("./axis-vars")]
};

View File

@@ -1,69 +1,49 @@
/******************************************************************************\
Copyright 2018. Buildbotics LLC
All Rights Reserved.
For information regarding this software email:
Joseph Coffland
joseph@buildbotics.com
This software is free software: you clan redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation, either version 2.1 of
the License, or (at your option) any later version.
This software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the C! library. If not, see
<http://www.gnu.org/licenses/>.
\******************************************************************************/
'use strict'
"use strict";
module.exports = function (prefix) {
if (typeof prefix == 'undefined') prefix = '';
var cookie = {
get: function (name, defaultValue) {
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
name = prefix + name + '=';
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1);
if (!c.indexOf(name)) return c.substring(name.length, c.length);
}
return defaultValue;
},
set: function (name, value, days) {
var offset = 2147483647; // Max value
if (typeof days != 'undefined') offset = days * 24 * 60 * 60 * 1000;
var d = new Date();
d.setTime(d.getTime() + offset);
var expires = 'expires=' + d.toUTCString();
document.cookie = prefix + name + '=' + value + ';' + expires + ';path=/';
},
set_bool: function (name, value) {
cookie.set(name, value ? 'true' : 'false');
},
get_bool: function (name, defaultValue) {
return cookie.get(name, defaultValue ? 'true' : 'false') == 'true';
if (typeof prefix == "undefined") {
prefix = "";
}
}
return cookie;
}
const cookie = {
get: function (name, defaultValue) {
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(";");
name = `${prefix + name}=`;
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (!c.indexOf(name)) {
return c.substring(name.length, c.length);
}
}
return defaultValue;
},
set: function (name, value, days) {
let offset = 2147483647; // Max value
if (typeof days != "undefined") {
offset = days * 24 * 60 * 60 * 1000;
}
const d = new Date();
d.setTime(d.getTime() + offset);
const expires = `expires=${d.toUTCString()}`;
document.cookie = `${prefix}${name}=${value};${expires};path=/`;
},
set_bool: function (name, value) {
cookie.set(name, value ? "true" : "false");
},
get_bool: function (name, defaultValue) {
return cookie.get(name, defaultValue ? "true" : "false") == "true";
}
};
return cookie;
};

View File

@@ -1,168 +1,160 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
var api = require('./api');
var entityMap = {
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
'/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;'}
"use strict";
const entityMap = {
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
"/": "&#x2F;", "`": "&#x60;", "=": "&#x3D;"};
function escapeHTML(s) {
return s.replace(/[&<>"'`=\/]/g, function (c) {return entityMap[c]})
return s.replace(/[&<>"'`=\\/]/g, function (c) {
return entityMap[c];
});
}
module.exports = {
template: '#gcode-viewer-template',
template: "#gcode-viewer-template",
data: function () {
return {
empty: true,
file: '',
line: -1,
scrolling: false
}
},
events: {
'gcode-load': function (file) {this.load(file)},
'gcode-clear': function () {this.clear()},
'gcode-reload': function (file) {this.reload(file)},
'gcode-line': function (line) {this.update_line(line)}
},
ready: function () {
this.clusterize = new Clusterize({
rows: [],
scrollElem: $(this.$el).find('.clusterize-scroll')[0],
contentElem: $(this.$el).find('.clusterize-content')[0],
no_data_text: 'GCode view...',
callbacks: {clusterChanged: this.highlight}
});
},
attached: function () {
if (typeof this.clusterize != 'undefined')
this.clusterize.refresh(true);
},
methods: {
load: async function(file) {
if (file == this.file) return;
this.clear();
this.file = file;
if (!file) return;
const response = await fetch(`/api/file/${file}?${Math.random()}`);
const text = await response.text();
if (text.length > 20e6) {
this.clusterize.update(['File is large - gcode view disabled']);
} else {
const lines = escapeHTML(text.trimRight())
.split(/[\r\n]/)
.map((line, i) => `<li class="ln${i + 1}"><b>${i + 1}</b>${line}</li>`);
this.clusterize.update(lines);
}
this.empty = false;
Vue.nextTick(this.update_line);
data: function () {
return {
empty: true,
file: "",
line: -1,
scrolling: false
};
},
clear: function () {
this.empty = true;
this.file = '';
this.line = -1;
this.clusterize.clear();
},
reload: function (file) {
if (file != this.file) return;
this.clear();
this.load(file);
},
highlight: function () {
var e = $(this.$el).find('.highlight');
if (e.length) e.removeClass('highlight');
e = $(this.$el).find('.ln' + this.line);
if (e.length) e.addClass('highlight');
},
update_line: function(line) {
if (typeof line != 'undefined') {
if (this.line == line) return;
this.line = line;
} else line = this.line;
var totalLines = this.clusterize.getRowsAmount();
if (line <= 0) line = 1;
if (totalLines < line) line = totalLines;
var e = $(this.$el).find('.clusterize-scroll');
var lineHeight = e[0].scrollHeight / totalLines;
var linesPerPage = Math.floor(e[0].clientHeight / lineHeight);
var current = e[0].scrollTop / lineHeight;
var target = line - 1 - Math.floor(linesPerPage / 2);
// Update scroll position
if (!this.scrolling) {
if (target < current - 20 || current + 20 < target)
e[0].scrollTop = target * lineHeight;
else {
this.scrolling = true;
e.animate({scrollTop: target * lineHeight}, {
complete: function () {this.scrolling = false}.bind(this)
})
events: {
"gcode-load": function (file) {
this.load(file);
},
"gcode-clear": function () {
this.clear();
},
"gcode-reload": function (file) {
this.reload(file);
},
"gcode-line": function (line) {
this.update_line(line);
}
}
},
Vue.nextTick(this.highlight);
ready: function () {
this.clusterize = new Clusterize({
rows: [],
scrollElem: $(this.$el).find(".clusterize-scroll")[0],
contentElem: $(this.$el).find(".clusterize-content")[0],
no_data_text: "GCode view...",
callbacks: {clusterChanged: this.highlight}
});
},
attached: function () {
if (typeof this.clusterize != "undefined") {
this.clusterize.refresh(true);
}
},
methods: {
load: async function(file) {
if (file == this.file) {
return;
}
this.clear();
this.file = file;
if (!file) {
return;
}
const response = await fetch(`/api/file/${file}?${Math.random()}`);
const text = await response.text();
if (text.length > 20e6) {
this.clusterize.update(["File is large - gcode view disabled"]);
} else {
const lines = escapeHTML(text.trimRight())
.split(/[\r\n]/)
.map((line, i) => `<li class="ln${i + 1}"><b>${i + 1}</b>${line}</li>`);
this.clusterize.update(lines);
}
this.empty = false;
Vue.nextTick(this.update_line);
},
clear: function () {
this.empty = true;
this.file = "";
this.line = -1;
this.clusterize.clear();
},
reload: function (file) {
if (file != this.file) {
return;
}
this.clear();
this.load(file);
},
highlight: function () {
let e = $(this.$el).find(".highlight");
if (e.length) {
e.removeClass("highlight");
}
e = $(this.$el).find(`.ln${this.line}`);
if (e.length) {
e.addClass("highlight");
}
},
update_line: function(line) {
if (typeof line != "undefined") {
if (this.line == line) {
return;
}
this.line = line;
} else {
line = this.line;
}
const totalLines = this.clusterize.getRowsAmount();
if (line <= 0) {
line = 1;
}
if (totalLines < line) {
line = totalLines;
}
const e = $(this.$el).find(".clusterize-scroll");
const lineHeight = e[0].scrollHeight / totalLines;
const linesPerPage = Math.floor(e[0].clientHeight / lineHeight);
const current = e[0].scrollTop / lineHeight;
const target = line - 1 - Math.floor(linesPerPage / 2);
// Update scroll position
if (!this.scrolling) {
if (target < current - 20 || current + 20 < target) {
e[0].scrollTop = target * lineHeight;
} else {
this.scrolling = true;
e.animate({scrollTop: target * lineHeight}, {
complete: function () {
this.scrolling = false;
}.bind(this)
});
}
}
Vue.nextTick(this.highlight);
}
}
}
}
};

View File

@@ -1,107 +1,94 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
var modbus = require('./modbus.js');
"use strict";
const modbus = require("./modbus.js");
module.exports = {
template: '#indicators-template',
props: ['state'],
template: "#indicators-template",
props: ["state"],
computed: {
modbus_status: function () {
return modbus.status_to_string(this.state.mx);
},
computed: {
modbus_status: function () {return modbus.status_to_string(this.state.mx)},
sense_error: function () {
let error = "";
if (this.state.motor_voltage_sense_error) {
error += "Motor voltage\n";
}
if (this.state.motor_current_sense_error) {
error += "Motor current\n";
}
if (this.state.load1_sense_error) {
error += "Load 1\n";
}
if (this.state.load2_sense_error) {
error += "Load 2\n";
}
if (this.state.vdd_current_sense_error) {
error += "Vdd current\n";
}
sense_error: function () {
var error = '';
return error;
}
},
if (this.state.motor_voltage_sense_error) error += 'Motor voltage\n';
if (this.state.motor_current_sense_error) error += 'Motor current\n';
if (this.state.load1_sense_error) error += 'Load 1\n';
if (this.state.load2_sense_error) error += 'Load 2\n';
if (this.state.vdd_current_sense_error) error += 'Vdd current\n';
methods: {
is_motor_enabled: function (motor) {
return typeof this.state[`${motor}me`] != "undefined" && this.state[`${motor}me`];
},
return error;
get_min_pin: function (motor) {
switch (motor) {
case 0: return 3;
case 1: return 5;
case 2: return 9;
case 3: return 11;
}
},
get_max_pin: function (motor) {
switch (motor) {
case 0: return 4;
case 1: return 8;
case 2: return 10;
case 3: return 12;
}
},
motor_fault_class: function (motor, bit) {
if (typeof motor == "undefined") {
const status = this.state["fa"];
if (typeof status == "undefined") {
return "fa-question";
}
return `fa-thumbs-${status ? "down error" : "up success"}`;
}
const flags = this.state[`${motor}df`];
if (typeof flags == "undefined") {
return "fa-question";
}
return (flags & (1 << bit)) ? "fa-thumbs-down error" :
"fa-thumbs-up success";
},
motor_reset: function (motor) {
if (typeof motor == "undefined") {
let cmd = "";
for (let i = 0; i < 4; i++) {
cmd += `\\$${i}df=0\n`;
}
this.$dispatch("send", cmd);
} else {
this.$dispatch("send", `\\$${motor}df=0`);
}
}
}
},
methods: {
is_motor_enabled: function (motor) {
return typeof this.state[motor + 'me'] != 'undefined' &&
this.state[motor + 'me'];
},
get_min_pin: function (motor) {
switch (motor) {
case 0: return 3;
case 1: return 5;
case 2: return 9;
case 3: return 11;
}
},
get_max_pin: function (motor) {
switch (motor) {
case 0: return 4;
case 1: return 8;
case 2: return 10;
case 3: return 12;
}
},
motor_fault_class: function (motor, bit) {
if (typeof motor == 'undefined') {
var status = this.state['fa'];
if (typeof status == 'undefined') return 'fa-question';
return 'fa-thumbs-' + (status ? 'down error' : 'up success')
}
var flags = this.state[motor + 'df'];
if (typeof flags == 'undefined') return 'fa-question';
return (flags & (1 << bit)) ? 'fa-thumbs-down error' :
'fa-thumbs-up success';
},
motor_reset: function (motor) {
if (typeof motor == 'undefined') {
var cmd = '';
for (var i = 0; i < 4; i++)
cmd += '\\$' + i + 'df=0\n';
this.$dispatch('send', cmd);
} else this.$dispatch('send', '\\$' + motor + 'df=0');
}
}
}
};

View File

@@ -1,177 +1,155 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
module.exports = {
template: "#io-indicator-template",
props: ['name', 'state'],
template: "#io-indicator-template",
props: ["name", "state"],
computed: {
klass: function () {
switch (this.name) {
case "min-switch-0": return this.get_motor_min_class(0);
case "min-switch-1": return this.get_motor_min_class(1);
case "min-switch-2": return this.get_motor_min_class(2);
case "min-switch-3": return this.get_motor_min_class(3);
case "max-switch-0": return this.get_motor_max_class(0);
case "max-switch-1": return this.get_motor_max_class(1);
case "max-switch-2": return this.get_motor_max_class(2);
case "max-switch-3": return this.get_motor_max_class(3);
case "estop": return this.get_input_class("ew", "et");
case "probe": return this.get_input_class("pw", "pt");
case "load-1": return this.get_output_class("1");
case "load-2": return this.get_output_class("2");
case "fault": return this.get_output_class("f");
case "tool-enable-mode": return this.get_output_class("e");
case "tool-direction-mode": return this.get_output_class("d");
}
},
computed: {
klass: function () {
if (this.name == 'min-switch-0') return this.get_motor_min_class(0);
if (this.name == 'min-switch-1') return this.get_motor_min_class(1);
if (this.name == 'min-switch-2') return this.get_motor_min_class(2);
if (this.name == 'min-switch-3') return this.get_motor_min_class(3);
if (this.name == 'max-switch-0') return this.get_motor_max_class(0);
if (this.name == 'max-switch-1') return this.get_motor_max_class(1);
if (this.name == 'max-switch-2') return this.get_motor_max_class(2);
if (this.name == 'max-switch-3') return this.get_motor_max_class(3);
if (this.name == 'estop') return this.get_input_class('ew', 'et');
if (this.name == 'probe') return this.get_input_class('pw', 'pt');
if (this.name == 'load-1') return this.get_output_class('1');
if (this.name == 'load-2') return this.get_output_class('2');
if (this.name == 'fault') return this.get_output_class('f');
if (this.name == 'tool-enable-mode') return this.get_output_class('e');
if (this.name == 'tool-direction-mode') return this.get_output_class('d');
tooltip: function () {
switch (this.name) {
case "min-switch-0": return this.get_motor_min_tooltip(0);
case "min-switch-1": return this.get_motor_min_tooltip(1);
case "min-switch-2": return this.get_motor_min_tooltip(2);
case "min-switch-3": return this.get_motor_min_tooltip(3);
case "max-switch-0": return this.get_motor_max_tooltip(0);
case "max-switch-1": return this.get_motor_max_tooltip(1);
case "max-switch-2": return this.get_motor_max_tooltip(2);
case "max-switch-3": return this.get_motor_max_tooltip(3);
case "estop": return this.get_input_tooltip("ew", "et");
case "probe": return this.get_input_tooltip("pw", "pt");
case "load-1": return this.get_output_tooltip("1");
case "load-2": return this.get_output_tooltip("2");
case "fault": return this.get_output_tooltip("f");
case "tool-direction-mode": return this.get_output_tooltip("d");
case "tool-enable-mode": return this.get_output_tooltip("e");
}
}
},
methods: {
get_io_state_class: function (active, state) {
if (typeof active == "undefined" || typeof state == "undefined") {
return "fa-exclamation-triangle warn";
}
tooltip: function () {
if (this.name == 'min-switch-0') return this.get_motor_min_tooltip(0);
if (this.name == 'min-switch-1') return this.get_motor_min_tooltip(1);
if (this.name == 'min-switch-2') return this.get_motor_min_tooltip(2);
if (this.name == 'min-switch-3') return this.get_motor_min_tooltip(3);
if (this.name == 'max-switch-0') return this.get_motor_max_tooltip(0);
if (this.name == 'max-switch-1') return this.get_motor_max_tooltip(1);
if (this.name == 'max-switch-2') return this.get_motor_max_tooltip(2);
if (this.name == 'max-switch-3') return this.get_motor_max_tooltip(3);
if (this.name == 'estop') return this.get_input_tooltip('ew', 'et');
if (this.name == 'probe') return this.get_input_tooltip('pw', 'pt');
if (this.name == 'load-1') return this.get_output_tooltip('1');
if (this.name == 'load-2') return this.get_output_tooltip('2');
if (this.name == 'fault') return this.get_output_tooltip('f');
if (this.name == 'tool-direction-mode')
return this.get_output_tooltip('d');
if (this.name == 'tool-enable-mode')
return this.get_output_tooltip('e');
if (state == 2) {
return "fa-circle-o";
}
const icon = state ? "fa-plus-circle" : "fa-minus-circle";
return `${icon} ${active ? "active" : "inactive"}`;
},
get_input_active: function (stateCode, typeCode) {
const type = this.state[typeCode];
const state = this.state[stateCode];
if (type == 1) {
return !state; // Normally open
} else if (type == 2) {
return state; // Normally closed
}
return false;
},
get_input_class: function (stateCode, typeCode) {
return this.get_io_state_class(this.get_input_active(stateCode, typeCode), this.state[stateCode]);
},
get_output_class: function (output) {
return this.get_io_state_class(this.state[`${output}oa`], this.state[`${output}os`]);
},
get_motor_min_class: function (motor) {
return this.get_input_class(`${motor}lw`, `${motor}ls`);
},
get_motor_max_class: function (motor) {
return this.get_input_class(`${motor}xw`, `${motor}xs`);
},
get_tooltip: function (mode, active, state) {
if (typeof mode == "undefined" || typeof active == "undefined" || typeof state == "undefined") {
return "Invalid";
}
if (state == 0) {
state = "Lo/Gnd";
} else if (state == 1) {
state = "Hi/+3.3v";
} else if (state == 2) {
state = "Tristated";
} else {
return "Invalid";
}
return `Mode: ${mode}\nActive: ${active ? "True" : "False"}\nLevel: ${state}`;
},
get_input_tooltip: function (stateCode, typeCode) {
let type = this.state[typeCode];
if (type == 0) {
return "Disabled";
} else if (type == 1) {
type = "Normally open";
} else if (type == 2) {
type = "Normally closed";
}
const active = this.get_input_active(stateCode, typeCode);
const state = this.state[stateCode];
return this.get_tooltip(type, active, state);
},
get_output_tooltip: function (output) {
let mode = this.state[`${output}om`];
switch (mode) {
case 0: return "Disabled";
case 1: mode = "Lo/Hi"; break;
case 2: mode = "Hi/Lo"; break;
case 3: mode = "Tri/Lo"; break;
case 4: mode = "Tri/Hi"; break;
case 5: mode = "Lo/Tri"; break;
case 6: mode = "Hi/Tri"; break;
default:
mode = undefined;
}
const active = this.state[`${output}oa`];
const state = this.state[`${output}os`];
return this.get_tooltip(mode, active, state);
},
get_motor_min_tooltip: function (motor) {
return this.get_input_tooltip(`${motor}lw`, `${motor}ls`);
},
get_motor_max_tooltip: function (motor) {
return this.get_input_tooltip(`${motor}xw`, `${motor}xs`);
}
}
},
methods: {
get_io_state_class: function (active, state) {
if (typeof active == 'undefined' || typeof state == 'undefined')
return 'fa-exclamation-triangle warn';
if (state == 2) return 'fa-circle-o';
return (state ? 'fa-plus-circle' : 'fa-minus-circle') + ' ' +
(active ? 'active' : 'inactive');
},
get_input_active: function (stateCode, typeCode) {
var type = this.state[typeCode];
var state = this.state[stateCode];
if (type == 1) return !state; // Normally open
else if (type == 2) return state; // Normally closed
return false
},
get_input_class: function (stateCode, typeCode) {
return this.get_io_state_class(this.get_input_active(stateCode, typeCode),
this.state[stateCode]);
},
get_output_class: function (output) {
return this.get_io_state_class(this.state[output + 'oa'],
this.state[output + 'os']);
},
get_motor_min_class: function (motor) {
return this.get_input_class(motor + 'lw', motor + 'ls');
},
get_motor_max_class: function (motor) {
return this.get_input_class(motor + 'xw', motor + 'xs');
},
get_tooltip: function (mode, active, state) {
if (typeof mode == 'undefined' || typeof active == 'undefined' ||
typeof state == 'undefined') return 'Invalid';
if (state == 0) state = 'Lo/Gnd';
else if (state == 1) state = 'Hi/+3.3v';
else if (state == 2) state = 'Tristated';
else return 'Invalid';
return 'Mode: ' + mode + '\nActive: ' + (active ? 'True' : 'False') +
'\nLevel: ' + state;
},
get_input_tooltip: function (stateCode, typeCode) {
var type = this.state[typeCode];
if (type == 0) return 'Disabled';
else if (type == 1) type = 'Normally open';
else if (type == 2) type = 'Normally closed';
var active = this.get_input_active(stateCode, typeCode);
var state = this.state[stateCode];
return this.get_tooltip(type, active, state);
},
get_output_tooltip: function (output) {
var mode = this.state[output + 'om'];
if (mode == 0) return 'Disabled';
else if (mode == 1) mode = 'Lo/Hi';
else if (mode == 2) mode = 'Hi/Lo';
else if (mode == 3) mode = 'Tri/Lo';
else if (mode == 4) mode = 'Tri/Hi';
else if (mode == 5) mode = 'Lo/Tri';
else if (mode == 6) mode = 'Hi/Tri';
else mode = undefined;
var active = this.state[output + 'oa'];
var state = this.state[output + 'os'];
return this.get_tooltip(mode, active, state);
},
get_motor_min_tooltip: function (motor) {
return this.get_input_tooltip(motor + 'lw', motor + 'ls');
},
get_motor_max_tooltip: function (motor) {
return this.get_input_tooltip(motor + 'xw', motor + 'xs');
}
}
}
};

View File

@@ -1,42 +1,13 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
module.exports = {
template: '#io-view-template',
props: ['config', 'template', 'state'],
template: "#io-view-template",
props: ["config", "template", "state"],
events: {
'input-changed': function() {
this.$dispatch('config-changed');
return false;
events: {
"input-changed": function() {
this.$dispatch("config-changed");
return false;
}
}
}
}
};

View File

@@ -1,148 +1,148 @@
'use strict';
"use strict";
function cookie_get(name) {
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
name = name + '=';
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(";");
name = `${name}=`;
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (!c.indexOf(name)) {
return c.substring(name.length, c.length);
if (!c.indexOf(name)) {
return c.substring(name.length, c.length);
}
}
}
}
function cookie_set(name, value, days) {
var d = new Date();
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
var expires = 'expires=' + d.toUTCString();
document.cookie = name + '=' + value + ';' + expires + ';path=/';
const d = new Date();
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
const expires = `expires=${d.toUTCString()}`;
document.cookie = `${name}=${value};${expires};path=/`;
}
var uuid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+';
const uuid_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+";
function uuid(length) {
if (typeof length == 'undefined') {
length = 52;
}
if (typeof length == "undefined") {
length = 52;
}
var s = '';
for (var i = 0; i < length; i++) {
s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)];
}
let s = "";
for (let i = 0; i < length; i++) {
s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)];
}
return s
return s;
}
$(function () {
if (typeof cookie_get('client-id') == 'undefined') {
cookie_set('client-id', uuid(), 10000);
}
// Register global components
Vue.component('templated-input', require('./templated-input'));
Vue.component('message', require('./message'));
Vue.component('indicators', require('./indicators'));
Vue.component('io-indicator', require('./io-indicator'));
Vue.component('console', require('./console'));
Vue.component('unit-value', require('./unit-value'));
Vue.filter('number', function (value) {
if (isNaN(value)) {
return 'NaN';
if (typeof cookie_get("client-id") == "undefined") {
cookie_set("client-id", uuid(), 10000);
}
return value.toLocaleString();
});
// Register global components
Vue.component("templated-input", require("./templated-input"));
Vue.component("message", require("./message"));
Vue.component("indicators", require("./indicators"));
Vue.component("io-indicator", require("./io-indicator"));
Vue.component("console", require("./console"));
Vue.component("unit-value", require("./unit-value"));
Vue.filter('percent', function (value, precision) {
if (typeof value == 'undefined') {
return '';
}
Vue.filter("number", function (value) {
if (isNaN(value)) {
return "NaN";
}
if (typeof precision == 'undefined') {
precision = 2;
}
return value.toLocaleString();
});
return (value * 100.0).toFixed(precision) + '%';
});
Vue.filter("percent", function (value, precision) {
if (typeof value == "undefined") {
return "";
}
Vue.filter('non_zero_percent', function (value, precision) {
if (!value) {
return '';
}
if (typeof precision == "undefined") {
precision = 2;
}
if (typeof precision == 'undefined') {
precision = 2;
}
return `${(value * 100.0).toFixed(precision)}%`;
});
return (value * 100.0).toFixed(precision) + '%';
});
Vue.filter("non_zero_percent", function (value, precision) {
if (!value) {
return "";
}
Vue.filter('fixed', function (value, precision) {
if (typeof value == 'undefined') {
return '0';
}
if (typeof precision == "undefined") {
precision = 2;
}
return parseFloat(value).toFixed(precision)
});
return `${(value * 100.0).toFixed(precision)}%`;
});
Vue.filter('upper', function (value) {
if (typeof value == 'undefined') {
return '';
}
Vue.filter("fixed", function (value, precision) {
if (typeof value == "undefined") {
return "0";
}
return value.toUpperCase()
});
return parseFloat(value).toFixed(precision);
});
Vue.filter('time', function (value, precision) {
if (isNaN(value)) {
return '';
}
Vue.filter("upper", function (value) {
if (typeof value == "undefined") {
return "";
}
if (isNaN(precision)) {
precision = 0;
}
return value.toUpperCase();
});
var MIN = 60;
var HR = MIN * 60;
var DAY = HR * 24;
var parts = [];
Vue.filter("time", function (value, precision) {
if (isNaN(value)) {
return "";
}
if (DAY <= value) {
parts.push(Math.floor(value / DAY));
value %= DAY;
}
if (isNaN(precision)) {
precision = 0;
}
if (HR <= value) {
parts.push(Math.floor(value / HR));
value %= HR;
}
const MIN = 60;
const HR = MIN * 60;
const DAY = HR * 24;
const parts = [];
if (MIN <= value) {
parts.push(Math.floor(value / MIN));
value %= MIN;
} else {
parts.push(0);
}
if (DAY <= value) {
parts.push(Math.floor(value / DAY));
value %= DAY;
}
parts.push(value);
if (HR <= value) {
parts.push(Math.floor(value / HR));
value %= HR;
}
for (var i = 0; i < parts.length; i++) {
parts[i] = parts[i].toFixed(i == parts.length - 1 ? precision : 0);
if (i && parts[i] < 10) {
parts[i] = '0' + parts[i];
}
}
if (MIN <= value) {
parts.push(Math.floor(value / MIN));
value %= MIN;
} else {
parts.push(0);
}
return parts.join(':');
});
parts.push(value);
// Vue app
require('./app');
for (let i = 0; i < parts.length; i++) {
parts[i] = parts[i].toFixed(i == parts.length - 1 ? precision : 0);
if (i && parts[i] < 10) {
parts[i] = `0${parts[i]}`;
}
}
return parts.join(":");
});
// Vue app
require("./app");
});

View File

@@ -1,47 +1,19 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
module.exports = {
template: '#message-template',
template: "#message-template",
props: {
show: {
type: Boolean,
required: true,
twoWay: true
},
props: {
show: {
type: Boolean,
required: true,
twoWay: true
},
class: {
type: String,
required: false,
twoWay: false
class: {
type: String,
required: false,
twoWay: false
}
}
}
}
};

View File

@@ -1,48 +1,20 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
module.exports = {
replace: true,
template: '#modbus-reg-view-template',
props: ['index', 'model', 'template', 'enable'],
replace: true,
template: "#modbus-reg-view-template",
props: ["index", "model", "template", "enable"],
computed: {
has_user_value: function () {
const type = this.model["reg-type"];
return type.indexOf("write") != -1 || type.indexOf("fixed") != -1;
}
},
computed: {
has_user_value: function () {
var type = this.model['reg-type'];
return type.indexOf('write') != -1 || type.indexOf('fixed') != -1;
methods: {
change: function () {
this.$dispatch("input-changed");
}
}
},
methods: {
change: function () {this.$dispatch('input-changed')}
}
}
};

View File

@@ -1,51 +1,23 @@
/******************************************************************************\
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
"use strict";
// Must match modbus.c
var exports = {
DISCONNECTED: 0,
OK: 1,
CRC: 2,
INVALID: 3,
TIMEDOUT: 4
const exports = {
DISCONNECTED: 0,
OK: 1,
CRC: 2,
INVALID: 3,
TIMEDOUT: 4
};
exports.status_to_string =
function (status) {
if (status == exports.OK) return 'Ok';
if (status == exports.CRC) return 'CRC error';
if (status == exports.INVALID) return 'Invalid response';
if (status == exports.TIMEDOUT) return 'Timedout';
return 'Disconnected';
}
switch (status) {
case exports.OK: return "Ok";
case exports.CRC: return "CRC error";
case exports.INVALID: return "Invalid response";
case exports.TIMEDOUT: return "Timedout";
default: return "Disconnected";
}
};
module.exports = exports;

View File

@@ -1,120 +1,120 @@
'use strict'
"use strict";
module.exports = {
template: '#motor-view-template',
props: ['index', 'config', 'template', 'state'],
template: "#motor-view-template",
props: ["index", "config", "template", "state"],
computed: {
metric: function () {
return this.$root.display_units === "METRIC";
},
computed: {
metric: function () {
return this.$root.display_units === "METRIC";
},
is_slave: function () {
for (var i = 0; i < this.index; i++) {
if (this.motor.axis == this.config.motors[i].axis) {
return true;
is_slave: function () {
for (let i = 0; i < this.index; i++) {
if (this.motor.axis == this.config.motors[i].axis) {
return true;
}
}
return false;
},
motor: function () {
return this.config.motors[this.index];
},
invalidMaxVelocity: function () {
return this.maxMaxVelocity < this.motor["max-velocity"];
},
maxMaxVelocity: function () {
return 1 * (15 * this.umPerStep / this.motor["microsteps"]).toFixed(3);
},
ustepPerSec: function () {
return this.rpm * this.stepsPerRev * this.motor["microsteps"] / 60;
},
rpm: function () {
return 1000 * this.motor["max-velocity"] / this.motor["travel-per-rev"];
},
gForce: function () {
return this.motor["max-accel"] * 0.0283254504;
},
gForcePerMin: function () {
return this.motor["max-jerk"] * 0.0283254504;
},
stepsPerRev: function () {
return 360 / this.motor["step-angle"];
},
umPerStep: function () {
return this.motor["travel-per-rev"] * this.motor["step-angle"] / 0.36;
},
milPerStep: function () {
return this.umPerStep / 25.4;
},
invalidStallVelocity: function () {
if (!this.motor["homing-mode"].startsWith("stall-")) {
return false;
}
return this.maxStallVelocity < this.motor["search-velocity"];
},
stallRPM: function () {
const v = this.motor["search-velocity"];
return 1000 * v / this.motor["travel-per-rev"];
},
maxStallVelocity: function () {
const maxRate = 900000 / this.motor["stall-sample-time"];
const ustep = this.motor["stall-microstep"];
const angle = this.motor["step-angle"];
const travel = this.motor["travel-per-rev"];
const maxStall = maxRate * 60 / 360 / 1000 * angle / ustep * travel;
return 1 * maxStall.toFixed(3);
},
stallUStepPerSec: function () {
const ustep = this.motor["stall-microstep"];
return this.stallRPM * this.stepsPerRev * ustep / 60;
}
}
return false;
},
motor: function () {
return this.config.motors[this.index]
},
events: {
"input-changed": function () {
Vue.nextTick(function () {
// Limit max-velocity
if (this.invalidMaxVelocity) {
this.$set('motor["max-velocity"]', this.maxMaxVelocity);
}
invalidMaxVelocity: function () {
return this.maxMaxVelocity < this.motor['max-velocity'];
},
//Limit stall-velocity
if (this.invalidStallVelocity) {
this.$set('motor["search-velocity"]', this.maxStallVelocity);
}
maxMaxVelocity: function () {
return 1 * (15 * this.umPerStep / this.motor['microsteps']).toFixed(3);
},
this.$dispatch("config-changed");
}.bind(this));
ustepPerSec: function () {
return this.rpm * this.stepsPerRev * this.motor['microsteps'] / 60;
},
rpm: function () {
return 1000 * this.motor['max-velocity'] / this.motor['travel-per-rev'];
},
gForce: function () {
return this.motor['max-accel'] * 0.0283254504
},
gForcePerMin: function () {
return this.motor['max-jerk'] * 0.0283254504
},
stepsPerRev: function () {
return 360 / this.motor['step-angle']
},
umPerStep: function () {
return this.motor['travel-per-rev'] * this.motor['step-angle'] / 0.36
},
milPerStep: function () {
return this.umPerStep / 25.4
},
invalidStallVelocity: function () {
if (!this.motor['homing-mode'].startsWith('stall-')) {
return false;
}
return this.maxStallVelocity < this.motor['search-velocity'];
},
stallRPM: function () {
var v = this.motor['search-velocity'];
return 1000 * v / this.motor['travel-per-rev'];
},
maxStallVelocity: function () {
var maxRate = 900000 / this.motor['stall-sample-time'];
var ustep = this.motor['stall-microstep'];
var angle = this.motor['step-angle'];
var travel = this.motor['travel-per-rev'];
var maxStall = maxRate * 60 / 360 / 1000 * angle / ustep * travel;
return 1 * maxStall.toFixed(3);
},
stallUStepPerSec: function () {
var ustep = this.motor['stall-microstep'];
return this.stallRPM * this.stepsPerRev * ustep / 60;
}
},
events: {
'input-changed': function () {
Vue.nextTick(function () {
// Limit max-velocity
if (this.invalidMaxVelocity) {
this.$set('motor["max-velocity"]', this.maxMaxVelocity);
return false;
}
},
//Limit stall-velocity
if (this.invalidStallVelocity) {
this.$set('motor["search-velocity"]', this.maxStallVelocity);
methods: {
show: function (name, templ) {
if (templ.hmodes == undefined) {
return true;
}
return templ.hmodes.indexOf(this.motor["homing-mode"]) != -1;
}
this.$dispatch('config-changed');
}.bind(this))
return false;
}
},
methods: {
show: function (name, templ) {
if (templ.hmodes == undefined) {
return true;
}
return templ.hmodes.indexOf(this.motor['homing-mode']) != -1;
}
}
}
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
module.exports = {
template: "#settings-view-template",
template: "#settings-view-template",
attached: function () {
this.svelteComponent = SvelteComponents.createComponent(
"SettingsView",
document.getElementById("settings")
);
},
attached: function () {
this.svelteComponent = SvelteComponents.createComponent(
"SettingsView",
document.getElementById("settings")
);
},
detached: function() {
this.svelteComponent.$destroy();
}
detached: function() {
this.svelteComponent.$destroy();
}
};

View File

@@ -1,127 +1,106 @@
/******************************************************************************\
"use strict";
This file is part of the Buildbotics firmware.
const Sock = function (url, retry, timeout) {
if (!(this instanceof Sock)) {
return new Sock(url, retry);
}
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
if (typeof retry == "undefined") {
retry = 2000;
}
if (typeof timeout == "undefined") {
timeout = 16000;
}
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
this.url = url;
this.retry = retry;
this.timeout = timeout;
this.divisions = 4;
this.count = 0;
The software 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
Lesser General Public License for more details.
this.connect();
};
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
Sock.prototype.onmessage = function () {
// Ignore
};
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict'
var Sock = function (url, retry, timeout) {
if (!(this instanceof Sock)) return new Sock(url, retry);
if (typeof retry == 'undefined') retry = 2000;
if (typeof timeout == 'undefined') timeout = 16000;
this.url = url;
this.retry = retry;
this.timeout = timeout;
this.divisions = 4;
this.count = 0;
this.connect();
}
Sock.prototype.onmessage = function () {}
Sock.prototype.onopen = function () {}
Sock.prototype.onclose = function () {}
Sock.prototype.onopen = function () {
// Ignore
};
Sock.prototype.onclose = function () {
// Ignore
};
Sock.prototype.connect = function () {
console.debug('connecting to', this.url);
this.close();
console.debug("connecting to", this.url);
this.close();
this._sock = new SockJS(this.url);
this._sock = new SockJS(this.url);
this._sock.onmessage = function (e) {
console.debug('msg:', e.data);
this.heartbeat('msg');
this.onmessage(e);
}.bind(this);
this._sock.onmessage = function (e) {
console.debug("msg:", e.data);
this.heartbeat("msg");
this.onmessage(e);
}.bind(this);
this._sock.onopen = function () {
console.debug("connected");
this.heartbeat("open");
this.onopen();
}.bind(this);
this._sock.onopen = function () {
console.debug('connected');
this.heartbeat('open');
this.onopen();
}.bind(this);
this._sock.onclose = function () {
console.debug('disconnected');
this._cancel_timeout();
this.onclose();
if (typeof this._sock != 'undefined')
setTimeout(this.connect.bind(this), this.retry);
}.bind(this);
}
this._sock.onclose = function () {
console.debug("disconnected");
this._cancel_timeout();
this.onclose();
if (typeof this._sock != "undefined") {
setTimeout(this.connect.bind(this), this.retry);
}
}.bind(this);
};
Sock.prototype._timedout = function () {
// Divide timeout so slow browser doesn't trigger timeouts when the
// connection is good.
if (this.divisions <= ++this.count) {
console.debug('connection timedout');
this._timeout = undefined;
this._sock.close();
} else this._set_timeout();
}
// Divide timeout so slow browser doesn't trigger timeouts when the
// connection is good.
if (this.divisions <= ++this.count) {
console.debug("connection timedout");
this._timeout = undefined;
this._sock.close();
} else {
this._set_timeout();
}
};
Sock.prototype._cancel_timeout = function () {
clearTimeout(this._timeout);
this._timeout = undefined;
this.count = 0;
}
clearTimeout(this._timeout);
this._timeout = undefined;
this.count = 0;
};
Sock.prototype._set_timeout = function () {
this._timeout = setTimeout(this._timedout.bind(this),
this.timeout / this.divisions);
}
Sock.prototype.heartbeat = function (msg) {
//console.debug('heartbeat ' + new Date().toLocaleTimeString() + ' ' + msg);
this._cancel_timeout();
this._set_timeout();
}
this._timeout = setTimeout(this._timedout.bind(this),
this.timeout / this.divisions);
};
Sock.prototype.heartbeat = function () {
this._cancel_timeout();
this._set_timeout();
};
Sock.prototype.close = function () {
if (typeof this._sock != 'undefined') {
var sock = this._sock;
this._sock = undefined;
sock.close();
}
}
if (typeof this._sock != "undefined") {
const sock = this._sock;
this._sock = undefined;
sock.close();
}
};
Sock.prototype.send = function (msg) {
this._sock.send(msg);
};
Sock.prototype.send = function (msg) {this._sock.send(msg)}
module.exports = Sock
module.exports = Sock;

View File

@@ -1,69 +1,69 @@
'use strict'
"use strict";
module.exports = {
replace: true,
template: '#templated-input-template',
props: ['name', 'model', 'template'],
replace: true,
template: "#templated-input-template",
props: ["name", "model", "template"],
data: function () {
return { view: '' }
},
computed: {
metric: function () {
return this.$root.display_units === "METRIC";
data: function () {
return { view: "" };
},
_view: function () {
if (this.template.scale) {
if (this.metric) {
return 1 * this.model.toFixed(3);
computed: {
metric: function () {
return this.$root.display_units === "METRIC";
},
_view: function () {
if (this.template.scale) {
if (this.metric) {
return 1 * this.model.toFixed(3);
}
return 1 * (this.model / this.template.scale).toFixed(4);
}
return this.model;
},
units: function () {
return (this.metric || !this.template.iunit)
? this.template.unit
: this.template.iunit;
},
title: function () {
let s = `Default :${this.template.default} ${(this.template.unit || "")}`;
if (typeof this.template.help != "undefined") {
s = `${this.template.help}\n${s}`;
}
return s;
}
return 1 * (this.model / this.template.scale).toFixed(4);
}
return this.model;
},
units: function () {
return (this.metric || !this.template.iunit)
? this.template.unit
: this.template.iunit;
watch: {
_view: function () {
this.view = this._view;
},
view: function () {
if (this.template.scale && !this.metric) {
this.model = this.view * this.template.scale;
} else {
this.model = this.view;
}
}
},
title: function () {
var s = `Default :${this.template.default} ${(this.template.unit || '')}`;
if (typeof this.template.help != 'undefined') {
s = this.template.help + '\n' + s;
}
return s;
}
},
watch: {
_view: function () {
this.view = this._view
ready: function () {
this.view = this._view;
},
view: function () {
if (this.template.scale && !this.metric) {
this.model = this.view * this.template.scale;
} else {
this.model = this.view;
}
methods: {
change: function () {
this.$dispatch("input-changed");
}
}
},
ready: function () {
this.view = this._view
},
methods: {
change: function () {
this.$dispatch('input-changed')
}
}
}
};

View File

@@ -1,277 +1,252 @@
/******************************************************************************\
"use strict";
This file is part of the Buildbotics firmware.
Copyright (c) 2015 - 2018, Buildbotics LLC
All rights reserved.
This file ("the software") is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License,
version 2 as published by the Free Software Foundation. You should
have received a copy of the GNU General Public License, version 2
along with the software. If not, see <http://www.gnu.org/licenses/>.
The software 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the software. If not, see
<http://www.gnu.org/licenses/>.
For information regarding this software email:
"Joseph Coffland" <joseph@buildbotics.com>
\******************************************************************************/
'use strict';
const api = require('./api');
const modbus = require('./modbus.js');
const api = require("./api");
const modbus = require("./modbus.js");
const merge = require("lodash.merge");
module.exports = {
template: '#tool-view-template',
props: ['config', 'template', 'state'],
template: "#tool-view-template",
props: ["config", "template", "state"],
data: function () {
return {
address: 0,
value: 0,
toolList: [
{
id: "disabled",
name: "Disabled"
},
{
id: "router",
type: "PWM Spindle",
name: "Router (Makita, etc)"
},
{
id: "laser",
type: "PWM Spindle",
name: "Laser (J Tech, etc)"
},
{
id: "pwm",
name: "PWM Spindle"
},
{
id: "unsupported-separator",
name: "Unsupported Tools",
disabled: true,
unsupported: true
},
{
id: "huanyang-vfd",
name: "Huanyang VFD",
unsupported: true
},
{
id: "custom-modbus-vfd",
name: "Custom Modbus VFD",
unsupported: true
},
{
id: "ac-tech-vfd",
name: "AC-Tech VFD",
unsupported: true
},
{
id: "nowforever-vfd",
name: "Nowforever VFD",
unsupported: true
},
{
id: "delta-vfd",
name: "Delta VFD015M21A (Beta)",
unsupported: true
},
{
id: "yl600-vfd",
name: "YL600, YL620, YL620-A VFD (Beta)",
unsupported: true
},
{
id: "fr-d700-vfd",
name: "FR-D700 (Beta)",
unsupported: true
},
{
id: "sunfar-e300-vfd",
name: "Sunfar E300 (Beta)",
unsupported: true
},
{
id: "omron-mx2-vfd",
name: "OMRON MX2",
unsupported: true
data: function () {
return {
address: 0,
value: 0,
toolList: [
{
id: "disabled",
name: "Disabled"
},
{
id: "router",
type: "PWM Spindle",
name: "Router (Makita, etc)"
},
{
id: "laser",
type: "PWM Spindle",
name: "Laser (J Tech, etc)"
},
{
id: "pwm",
name: "PWM Spindle"
},
{
id: "unsupported-separator",
name: "Unsupported Tools",
disabled: true,
unsupported: true
},
{
id: "huanyang-vfd",
name: "Huanyang VFD",
unsupported: true
},
{
id: "custom-modbus-vfd",
name: "Custom Modbus VFD",
unsupported: true
},
{
id: "ac-tech-vfd",
name: "AC-Tech VFD",
unsupported: true
},
{
id: "nowforever-vfd",
name: "Nowforever VFD",
unsupported: true
},
{
id: "delta-vfd",
name: "Delta VFD015M21A (Beta)",
unsupported: true
},
{
id: "yl600-vfd",
name: "YL600, YL620, YL620-A VFD (Beta)",
unsupported: true
},
{
id: "fr-d700-vfd",
name: "FR-D700 (Beta)",
unsupported: true
},
{
id: "sunfar-e300-vfd",
name: "Sunfar E300 (Beta)",
unsupported: true
},
{
id: "omron-mx2-vfd",
name: "OMRON MX2",
unsupported: true
}
]
};
},
components: {
"modbus-reg": require("./modbus-reg.js")
},
watch: {
"state.mr": function () {
this.value = this.state.mr;
}
},
events: {
"input-changed": function () {
this.$dispatch("config-changed");
return false;
},
},
ready: function () {
this.value = this.state.mr;
},
computed: {
regs_tmpl: function () {
return this.template["modbus-spindle"].regs;
},
tool_type: function () {
return this.config.tool["tool-type"].toUpperCase();
},
selected_tool: function () {
return this.config.tool["selected-tool"];
},
is_pwm_spindle: function () {
return this.selected_tool == "pwm";
},
is_modbus: function () {
switch (this.selected_tool) {
case "disabled":
case "laser":
case "router":
case "pwm":
return false;
default:
return true;
}
},
modbus_status: function () {
return modbus.status_to_string(this.state.mx);
}
},
methods: {
change_selected_tool: function () {
const selectedToolSettings = this.config["selected-tool-settings"] || {};
const settings = selectedToolSettings[this.selected_tool] || {};
this.config.tool = merge({}, this.config.tool, settings["tool"]);
this.config["pwm-spindle"] = merge({}, this.config["pwm-spindle"], settings["pwm-spindle"]);
this.config["modbus-spindle"] = merge({}, this.config["modbus-spindle"], settings["modbus-spindle"]);
const tool = this.toolList.find(tool => tool.id == this.config.tool["selected-tool"]);
this.config.tool["tool-type"] = tool.type || tool.name;
this.$dispatch("config-changed");
},
show_tool_settings: function (key) {
switch (true) {
case key === "tool-type":
case key === "selected-tool":
return false;
case this.selected_tool === "disabled":
return false;
case this.selected_tool === "laser":
case this.selected_tool === "router":
switch (key) {
case "tool-enable-mode":
return true;
default:
return false;
}
default:
return true;
}
},
get_reg_type: function (reg) {
return this.regs_tmpl.template["reg-type"].values[this.state[`${reg}vt`]];
},
get_reg_addr: function (reg) {
return this.state[`${reg}va`];
},
get_reg_value: function (reg) {
return this.state[`${reg}vv`];
},
get_reg_fails: function (reg) {
const fails = this.state[`${reg}vr`];
return fails == 255 ? "Max" : fails;
},
show_modbus_field: function (key) {
return key != "regs" && (key != "multi-write" || this.tool_type == "CUSTOM MODBUS VFD");
},
read: function (e) {
e.preventDefault();
api.put("modbus/read", { address: this.address });
},
write: function (e) {
e.preventDefault();
api.put("modbus/write", { address: this.address, value: this.value });
},
customize: function (e) {
e.preventDefault();
this.config.tool["tool-type"] = "Custom Modbus VFD";
const regs = this.config["modbus-spindle"].regs;
for (let i = 0; i < regs.length; i++) {
const reg = this.regs_tmpl.index[i];
regs[i]["reg-type"] = this.get_reg_type(reg);
regs[i]["reg-addr"] = this.get_reg_addr(reg);
regs[i]["reg-value"] = this.get_reg_value(reg);
}
this.$dispatch("config-changed");
},
clear: function (e) {
e.preventDefault();
this.config.tool["tool-type"] = "Custom Modbus VFD";
const regs = this.config["modbus-spindle"].regs;
for (let i = 0; i < regs.length; i++) {
regs[i]["reg-type"] = "disabled";
regs[i]["reg-addr"] = 0;
regs[i]["reg-value"] = 0;
}
this.$dispatch("config-changed");
},
reset_failures: function (e) {
e.preventDefault();
const regs = this.config["modbus-spindle"].regs;
for (let reg = 0; reg < regs.length; reg++) {
this.$dispatch("send", `$${reg}vr=0`);
}
}
]
}
},
components: {
'modbus-reg': require('./modbus-reg.js')
},
watch: {
'state.mr': function () { this.value = this.state.mr }
},
events: {
'input-changed': function () {
this.$dispatch('config-changed');
return false;
},
},
ready: function () {
this.value = this.state.mr;
},
computed: {
regs_tmpl: function () {
return this.template['modbus-spindle'].regs;
},
tool_type: function () {
return this.config.tool['tool-type'].toUpperCase();
},
selected_tool: function () {
return this.config.tool['selected-tool'];
},
is_pwm_spindle: function () {
return this.selected_tool == 'pwm';
},
is_modbus: function () {
switch (this.selected_tool) {
case "disabled":
case "laser":
case "router":
case "pwm":
return false;
default:
return true;
}
},
modbus_status: function () {
return modbus.status_to_string(this.state.mx);
}
},
methods: {
change_selected_tool: function () {
const selectedToolSettings = this.config['selected-tool-settings'] || {};
const settings = selectedToolSettings[this.selected_tool] || {};
this.config.tool = merge({}, this.config.tool, settings['tool']);
this.config['pwm-spindle'] = merge({}, this.config['pwm-spindle'], settings['pwm-spindle']);
this.config['modbus-spindle'] = merge({}, this.config['modbus-spindle'], settings['modbus-spindle']);
const tool = this.toolList.find(tool => tool.id == this.config.tool['selected-tool']);
this.config.tool["tool-type"] = tool.type || tool.name;
this.$dispatch("config-changed");
},
show_tool_settings: function (key) {
switch (true) {
case key === "tool-type":
case key === "selected-tool":
return false;
case this.selected_tool === "disabled":
return false;
case this.selected_tool === "laser":
case this.selected_tool === "router":
switch (key) {
case "tool-enable-mode":
return true;
default:
return false;
}
default:
return true;
}
},
get_reg_type: function (reg) {
return this.regs_tmpl.template['reg-type'].values[this.state[reg + 'vt']];
},
get_reg_addr: function (reg) {
return this.state[reg + 'va'];
},
get_reg_value: function (reg) {
return this.state[reg + 'vv'];
},
get_reg_fails: function (reg) {
const fails = this.state[reg + 'vr']
return fails == 255 ? 'Max' : fails;
},
show_modbus_field: function (key) {
return key != 'regs' &&
(key != 'multi-write' || this.tool_type == 'CUSTOM MODBUS VFD');
},
read: function (e) {
e.preventDefault();
api.put('modbus/read', { address: this.address });
},
write: function (e) {
e.preventDefault();
api.put('modbus/write', { address: this.address, value: this.value });
},
customize: function (e) {
e.preventDefault();
this.config.tool['tool-type'] = 'Custom Modbus VFD';
const regs = this.config['modbus-spindle'].regs;
for (let i = 0; i < regs.length; i++) {
const reg = this.regs_tmpl.index[i];
regs[i]['reg-type'] = this.get_reg_type(reg);
regs[i]['reg-addr'] = this.get_reg_addr(reg);
regs[i]['reg-value'] = this.get_reg_value(reg);
}
this.$dispatch('config-changed');
},
clear: function (e) {
e.preventDefault();
this.config.tool['tool-type'] = 'Custom Modbus VFD';
const regs = this.config['modbus-spindle'].regs;
for (let i = 0; i < regs.length; i++) {
regs[i]['reg-type'] = 'disabled';
regs[i]['reg-addr'] = 0;
regs[i]['reg-value'] = 0;
}
this.$dispatch('config-changed');
},
reset_failures: function (e) {
e.preventDefault();
const regs = this.config['modbus-spindle'].regs;
for (let reg = 0; reg < regs.length; reg++)
this.$dispatch('send', '\$' + reg + 'vr=0');
}
}
}
};

View File

@@ -1,47 +1,47 @@
'use strict'
"use strict";
module.exports = {
replace: true,
template: '{{text}}<span class="unit">{{metric ? unit : iunit}}</span>',
props: ['value', 'precision', 'unit', 'iunit', 'scale'],
replace: true,
template: '{{text}}<span class="unit">{{metric ? unit : iunit}}</span>',
props: ["value", "precision", "unit", "iunit", "scale"],
computed: {
metric: {
cache: false,
get: function () {
return this.$root.display_units === "METRIC";
}
computed: {
metric: {
cache: false,
get: function () {
return this.$root.display_units === "METRIC";
}
},
text: function () {
let value = this.value;
if (typeof value == "undefined") {
return "";
}
if (!this.metric) {
value /= this.scale;
}
return (1 * value.toFixed(this.precision)).toLocaleString();
}
},
text: function () {
var value = this.value;
if (typeof value == 'undefined') {
return '';
}
ready: function () {
if (typeof this.precision == "undefined") {
this.precision = 0;
}
if (!this.metric) {
value /= this.scale;
}
if (typeof this.unit == "undefined") {
this.unit = "mm";
}
return (1 * value.toFixed(this.precision)).toLocaleString();
if (typeof this.iunit == "undefined") {
this.iunit = "in";
}
if (typeof this.scale == "undefined") {
this.scale = 25.4;
}
}
},
ready: function () {
if (typeof this.precision == 'undefined') {
this.precision = 0;
}
if (typeof this.unit == 'undefined') {
this.unit = 'mm';
}
if (typeof this.iunit == 'undefined') {
this.iunit = 'in';
}
if (typeof this.scale == 'undefined') {
this.scale = 25.4;
}
}
}
};

9
src/js/utils.js Normal file
View File

@@ -0,0 +1,9 @@
function clickFileInput(formClass) {
const form = document.querySelector(`.${formClass}`);
form.reset();
form.querySelector("input").click();
}
module.exports = {
clickFileInput
};

View File

@@ -1,204 +1,210 @@
<script lang="ts">
import WifiConnectionDialog from "$dialogs/WifiConnectionDialog.svelte";
import ChangeHostnameDialog from "$dialogs/ChangeHostnameDialog.svelte";
import Button, { Label } from "@smui/button";
import List, { Item, Graphic, Text, Meta } from "@smui/list";
import Card from "@smui/card";
import { networkInfo, type WifiNetwork } from "$lib/NetworkInfo";
import WifiConnectionDialog from "$dialogs/WifiConnectionDialog.svelte";
import ChangeHostnameDialog from "$dialogs/ChangeHostnameDialog.svelte";
import Button, { Label } from "@smui/button";
import List, { Item, Graphic, Text, Meta } from "@smui/list";
import Card from "@smui/card";
import { networkInfo, type WifiNetwork } from "$lib/NetworkInfo";
let changeHostnameDialog = {
open: false,
};
let changeHostnameDialog = {
open: false,
};
let wifiConnectionDialog = {
open: false,
network: {} as WifiNetwork,
};
let wifiConnectionDialog = {
open: false,
network: {} as WifiNetwork,
};
function getWifiStrengthStyle(network: WifiNetwork) {
const strength = Math.ceil((Number(network.Quality) / 100) * 4);
function getWifiStrengthStyle(network: WifiNetwork) {
const strength = Math.ceil((Number(network.Quality) / 100) * 4);
switch (strength) {
case 0:
return "clip-path: circle(0px at 12.5px 19px);";
switch (strength) {
case 0:
return "clip-path: circle(0px at 12.5px 19px);";
case 1:
return "clip-path: circle(4px at 12.5px 19px);";
case 1:
return "clip-path: circle(4px at 12.5px 19px);";
case 2:
return "clip-path: circle(8px at 12.5px 19px);";
case 2:
return "clip-path: circle(8px at 12.5px 19px);";
case 3:
return "clip-path: circle(14px at 12.5px 19px);";
case 3:
return "clip-path: circle(14px at 12.5px 19px);";
case 4:
return "";
case 4:
return "";
}
}
}
function onChangeHostname() {
changeHostnameDialog = {
open: true,
};
}
function onChangeHostname() {
changeHostnameDialog = {
open: true,
};
}
function onNetworkSelected(network: WifiNetwork) {
wifiConnectionDialog = {
open: true,
network,
};
}
function onNetworkSelected(network: WifiNetwork) {
wifiConnectionDialog = {
open: true,
network,
};
}
</script>
<WifiConnectionDialog {...wifiConnectionDialog} />
<ChangeHostnameDialog {...changeHostnameDialog} />
<div class="admin-network-view">
<h1>Network Info</h1>
<h1>Network Info</h1>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="hostname">Hostname</label>
<Card id="hostname" variant="outlined">
<Text id="hostname">
{$networkInfo.hostname}
</Text>
</Card>
<Button on:click={onChangeHostname} touch variant="raised">
<Label>Change</Label>
</Button>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="hostname">Hostname</label>
<Card id="hostname" variant="outlined">
<Text id="hostname">
{$networkInfo.hostname}
</Text>
</Card>
<Button on:click={onChangeHostname} touch variant="raised">
<Label>Change</Label>
</Button>
</div>
</div>
</div>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="ip-addresses">IP Addresses</label>
<Card id="ip-addresses" variant="outlined">
{#each $networkInfo.ipAddresses as ipAddress}
<div>
<Text id="hostname">
{ipAddress}
</Text>
</div>
{/each}
</Card>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="ip-addresses">IP Addresses</label>
<Card id="ip-addresses" variant="outlined">
{#each $networkInfo.ipAddresses as ipAddress}
<div>
<Text id="hostname">
{ipAddress}
</Text>
</div>
{/each}
</Card>
</div>
</div>
</div>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="wifi">Wi-Fi</label>
<div class="wifi-networks">
<Card id="wifi" variant="outlined">
<List>
{#if $networkInfo.wifi.networks.length === 0}
<Item class="wifi-network">
<Text>Scanning...</Text>
</Item>
{:else}
{#each $networkInfo.wifi.networks as network}
<Item
class="wifi-network"
on:SMUI:action={() => onNetworkSelected(network)}
>
<Graphic
class="strength {$networkInfo.wifi.ssid === network.Name
? 'active'
: ''}"
>
<span class="fa fa-wifi background" />
<span
class="fa fa-wifi"
style={getWifiStrengthStyle(network)}
/>
</Graphic>
<Text style="margin-right: 20px;">{network.Name}</Text>
{#if network.Encryption !== "Open"}
<Meta>
<span class="fa fa-lock" />
</Meta>
{/if}
</Item>
{/each}
{/if}
</List>
</Card>
<em style="display: block;">
Click on a Wi-Fi network to connect or disconnect.
</em>
</div>
<div class="pure-form pure-form-aligned">
<div class="pure-control-group">
<label for="wifi">Wi-Fi</label>
<div class="wifi-networks">
<Card id="wifi" variant="outlined">
<List>
{#if $networkInfo.wifi.networks.length === 0}
<Item class="wifi-network">
<Text>Scanning...</Text>
</Item>
{:else}
{#each $networkInfo.wifi.networks as network}
<Item
class="wifi-network"
on:SMUI:action={() =>
onNetworkSelected(network)}
>
<Graphic
class="strength {$networkInfo.wifi
.ssid === network.Name
? 'active'
: ''}"
>
<span class="fa fa-wifi background" />
<span
class="fa fa-wifi"
style={getWifiStrengthStyle(
network
)}
/>
</Graphic>
<Text style="margin-right: 20px;"
>{network.Name}</Text
>
{#if network.Encryption !== "Open"}
<Meta>
<span class="fa fa-lock" />
</Meta>
{/if}
</Item>
{/each}
{/if}
</List>
</Card>
<em style="display: block;">
Click on a Wi-Fi network to connect or disconnect.
</em>
</div>
</div>
</div>
</div>
</div>
<style lang="scss">
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
:global {
.admin-network-view {
.pure-form-aligned .pure-control-group label {
vertical-align: top;
font-size: 15pt;
font-weight: bold;
}
button {
margin: 0;
}
.mdc-card {
width: 400px;
min-height: 38px;
display: inline-block;
vertical-align: top;
margin-bottom: 20px;
margin-right: 20px;
padding: 5px 15px;
}
.wifi-networks {
display: inline-block;
.mdc-card {
padding: 0;
margin-bottom: 5px;
}
}
.wifi-network {
.lock {
font-size: 20px;
vertical-align: text-bottom;
}
.strength {
border-radius: 50%;
padding: 3px;
background-color: $light;
color: $very-dark;
margin-right: 10px;
position: relative;
&.active {
background-color: $primary;
color: white;
}
span {
position: absolute;
top: 5px;
font-size: 22px;
&.background {
opacity: 0.25;
:global {
.admin-network-view {
.pure-form-aligned .pure-control-group label {
vertical-align: top;
font-size: 15pt;
font-weight: bold;
}
button {
margin: 0;
}
.mdc-card {
width: 400px;
min-height: 38px;
display: inline-block;
vertical-align: top;
margin-bottom: 20px;
margin-right: 20px;
padding: 5px 15px;
}
.wifi-networks {
display: inline-block;
.mdc-card {
padding: 0;
margin-bottom: 5px;
}
}
.wifi-network {
.lock {
font-size: 20px;
vertical-align: text-bottom;
}
.strength {
border-radius: 50%;
padding: 3px;
background-color: $light;
color: $very-dark;
margin-right: 10px;
position: relative;
&.active {
background-color: $primary;
color: white;
}
span {
position: absolute;
top: 5px;
font-size: 22px;
&.background {
opacity: 0.25;
}
}
}
}
}
}
}
}
}
</style>

View File

@@ -1,131 +1,131 @@
<script lang="ts">
import configTemplate from "../../../resources/config-template.json";
import { Config, DisplayUnits } from "$lib/ConfigStore";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { onMount } from "svelte";
import configTemplate from "../../../resources/config-template.json";
import { Config, DisplayUnits } from "$lib/ConfigStore";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { onMount } from "svelte";
type Template = {
type?: string;
values?: (string | number)[];
unit?: "string";
iunit?: "string";
min?: number;
max?: number;
step?: number;
help?: string;
default?: string | number;
scale?: number;
};
type Template = {
type?: string;
values?: (string | number)[];
unit?: "string";
iunit?: "string";
min?: number;
max?: number;
step?: number;
help?: string;
default?: string | number;
scale?: number;
};
export let key: string;
let keyParts: string[];
let template: Template;
let name: string;
let title: string;
let units: string;
let value;
export let key: string;
let keyParts: string[];
let template: Template;
let name: string;
let title: string;
let units: string;
let value;
onMount(() => {
keyParts = (key || "").split(".");
template = getTemplate();
name = keyParts[keyParts.length - 1];
title = getTitle();
value = getValue();
});
$: metric = $DisplayUnits === "METRIC";
$: if (template) {
units = metric || !template.iunit ? template.unit : template.iunit;
}
function getTemplate(): Template {
let template = configTemplate;
for (const part of keyParts) {
template = template[part];
}
return template as Template;
}
function getTitle(): string {
const help = template.help ? `${template.help}\n` : "";
return `${help}Default: ${template.default} ${template.unit || ""}`;
}
function getValue(): string | number {
let value: any = $Config;
for (const part of keyParts) {
value = value[part];
}
if (template.scale) {
if (metric) {
return Number.parseFloat(value.toFixed(3));
}
return Number.parseFloat((value / template.scale).toFixed(4));
}
return value;
}
function onChange() {
Config.update((config) => {
let target = config;
for (const part of keyParts.slice(0, -1)) {
target = target[part];
}
target[keyParts[keyParts.length - 1]] = value;
return config;
onMount(() => {
keyParts = (key || "").split(".");
template = getTemplate();
name = keyParts[keyParts.length - 1];
title = getTitle();
value = getValue();
});
ControllerMethods.dispatch("config-changed");
}
$: metric = $DisplayUnits === "METRIC";
$: if (template) {
units = metric || !template.iunit ? template.unit : template.iunit;
}
function getTemplate(): Template {
let template = configTemplate;
for (const part of keyParts) {
template = template[part];
}
return template as Template;
}
function getTitle(): string {
const help = template.help ? `${template.help}\n` : "";
return `${help}Default: ${template.default} ${template.unit || ""}`;
}
function getValue(): string | number {
let value: any = $Config;
for (const part of keyParts) {
value = value[part];
}
if (template.scale) {
if (metric) {
return Number.parseFloat(value.toFixed(3));
}
return Number.parseFloat((value / template.scale).toFixed(4));
}
return value;
}
function onChange() {
Config.update((config) => {
let target = config;
for (const part of keyParts.slice(0, -1)) {
target = target[part];
}
target[keyParts[keyParts.length - 1]] = value;
return config;
});
ControllerMethods.dispatch("config-changed");
}
</script>
{#if template}
<div class="pure-control-group" {title}>
<label for={name}>{name}</label>
<div class="pure-control-group" {title}>
<label for={name}>{name}</label>
{#if template.values}
<select {name} bind:value on:change={onChange}>
{#each template.values as opt}
<option value={opt} disabled={opt === "-----"}>
{opt}
</option>
{/each}
</select>
{:else if template.type === "bool"}
<input {name} type="checkbox" bind:value on:input={onChange} />
{:else if template.type === "float"}
<input
{name}
type="number"
min={template.min}
max={template.max}
step={template.step || "any"}
bind:value
on:input={onChange}
/>
{:else if template.type === "int"}
<input
{name}
type="number"
min={template.min}
max={template.max}
bind:value
on:input={onChange}
/>
{:else if template.type === "string"}
<input {name} type="text" bind:value on:input={onChange} />
{:else if template.type == "text"}
<textarea {name} bind:value on:input={onChange} />
{/if}
{#if template.values}
<select {name} bind:value on:change={onChange}>
{#each template.values as opt}
<option value={opt} disabled={opt === "-----"}>
{opt}
</option>
{/each}
</select>
{:else if template.type === "bool"}
<input {name} type="checkbox" bind:value on:input={onChange} />
{:else if template.type === "float"}
<input
{name}
type="number"
min={template.min}
max={template.max}
step={template.step || "any"}
bind:value
on:input={onChange}
/>
{:else if template.type === "int"}
<input
{name}
type="number"
min={template.min}
max={template.max}
bind:value
on:input={onChange}
/>
{:else if template.type === "string"}
<input {name} type="text" bind:value on:input={onChange} />
{:else if template.type == "text"}
<textarea {name} bind:value on:input={onChange} />
{/if}
<label for="" class="units">{units || ""}</label>
<label for="" class="units">{units || ""}</label>
<slot name="extra" />
</div>
<slot name="extra" />
</div>
{/if}

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import configTemplate from "../../../resources/config-template.json";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
import Button, { Label } from "@smui/button";
import configTemplate from "../../../resources/config-template.json";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
import Button, { Label } from "@smui/button";
const gcodeURL = "https://linuxcnc.org/docs/html/gcode/g-code.html";
const gcodeURL = "https://linuxcnc.org/docs/html/gcode/g-code.html";
let showScreenRotationDialog = false;
let showSetTimeDialog = false;
let showScreenRotationDialog = false;
let showSetTimeDialog = false;
</script>
<ScreenRotationDialog bind:open={showScreenRotationDialog} />
@@ -17,81 +17,81 @@
<h1>Settings</h1>
<div class="pure-form pure-form-aligned">
<h2>User Interface</h2>
<fieldset>
<div class="pure-control-group">
<label for="screen-rotation" />
<Button
name="screen-rotation"
touch
variant="raised"
on:click={() => (showScreenRotationDialog = true)}
>
<Label>Change Screen Rotation</Label>
</Button>
</div>
<h2>User Interface</h2>
<fieldset>
<div class="pure-control-group">
<label for="screen-rotation" />
<Button
name="screen-rotation"
touch
variant="raised"
on:click={() => (showScreenRotationDialog = true)}
>
<Label>Change Screen Rotation</Label>
</Button>
</div>
<div class="pure-control-group">
<label for="set-time" />
<Button
name="set-time"
touch
variant="raised"
on:click={() => (showSetTimeDialog = true)}
>
<Label>Change Time & Timezone</Label>
</Button>
</div>
</fieldset>
<div class="pure-control-group">
<label for="set-time" />
<Button
name="set-time"
touch
variant="raised"
on:click={() => (showSetTimeDialog = true)}
>
<Label>Change Time & Timezone</Label>
</Button>
</div>
</fieldset>
<fieldset>
<h2>Probe Dimensions</h2>
{#each Object.keys(configTemplate.probe) as key}
{#if key !== "probe-diameter"}
<ConfigTemplatedInput key={`probe.${key}`} />
{/if}
{/each}
</fieldset>
<fieldset>
<h2>Probe Dimensions</h2>
{#each Object.keys(configTemplate.probe) as key}
{#if key !== "probe-diameter"}
<ConfigTemplatedInput key={`probe.${key}`} />
{/if}
{/each}
</fieldset>
<fieldset>
<h2>GCode</h2>
{#each Object.keys(configTemplate.gcode) as key}
<ConfigTemplatedInput key={`gcode.${key}`} />
{/each}
</fieldset>
<fieldset>
<h2>GCode</h2>
{#each Object.keys(configTemplate.gcode) as key}
<ConfigTemplatedInput key={`gcode.${key}`} />
{/each}
</fieldset>
<h2>Path Accuracy</h2>
<fieldset>
<ConfigTemplatedInput key={`settings.max-deviation`} />
</fieldset>
<h2>Path Accuracy</h2>
<fieldset>
<ConfigTemplatedInput key={`settings.max-deviation`} />
</fieldset>
<p>
Lower <tt>max-deviation</tt> to follow the programmed path more precisely but
at a slower speed.
</p>
<p>
Lower <tt>max-deviation</tt> to follow the programmed path more precisely
but at a slower speed.
</p>
<p>
In order to improve traversal speed, the path planner may merge consecutive
moves or round off sharp corners if doing so would deviate from the program
path by less than <tt>max-deviation</tt>.
</p>
<p>
In order to improve traversal speed, the path planner may merge
consecutive moves or round off sharp corners if doing so would deviate
from the program path by less than <tt>max-deviation</tt>.
</p>
<p>
GCode commands
<a href={`${gcodeURL}#gcode:g61`} target="_blank">G61, G61.1</a>
and <a href={`${gcodeURL}#gcode:g64`} target="_blank"> G64</a> also affect path
planning accuracy.
</p>
<p>
GCode commands
<a href={`${gcodeURL}#gcode:g61`} target="_blank">G61, G61.1</a>
and <a href={`${gcodeURL}#gcode:g64`} target="_blank"> G64</a> also affect
path planning accuracy.
</p>
<h2>Cornering Speed (Advanced)</h2>
<fieldset>
<ConfigTemplatedInput key={`settings.junction-accel`} />
</fieldset>
<h2>Cornering Speed (Advanced)</h2>
<fieldset>
<ConfigTemplatedInput key={`settings.junction-accel`} />
</fieldset>
<p>
Junction acceleration limits the cornering speed the planner will allow.
Increasing this value will allow for faster traversal of corners but may
cause the planner to violate axis jerk limits and stall the motors. Use with
caution.
</p>
<p>
Junction acceleration limits the cornering speed the planner will allow.
Increasing this value will allow for faster traversal of corners but may
cause the planner to violate axis jerk limits and stall the motors. Use
with caution.
</p>
</div>

View File

@@ -1,90 +1,90 @@
<script lang="ts">
import TextField from "@smui/textfield";
import Icon from "@smui/textfield/icon";
import HelperText from "@smui/textfield/helper-text";
import MenuSurface, {
type MenuSurfaceComponentDev,
} from "@smui/menu-surface";
import List, { Item, Text } from "@smui/list";
import { virtualKeyboardChange } from "$lib/CustomActions";
import { onDestroy } from "svelte";
import TextField from "@smui/textfield";
import Icon from "@smui/textfield/icon";
import HelperText from "@smui/textfield/helper-text";
import MenuSurface, {
type MenuSurfaceComponentDev,
} from "@smui/menu-surface";
import List, { Item, Text } from "@smui/list";
import { virtualKeyboardChange } from "$lib/CustomActions";
import { onDestroy } from "svelte";
let menuSurface: MenuSurfaceComponentDev;
let menuTimeout;
let optionSelected: boolean = false;
let menuSurface: MenuSurfaceComponentDev;
let menuTimeout;
let optionSelected: boolean = false;
export let value: string;
export let options: string[][];
export let valid: boolean;
export let helperText: string;
export let value: string;
export let options: string[][];
export let valid: boolean;
export let helperText: string;
onDestroy(() => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = undefined;
onDestroy(() => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = undefined;
}
});
function showMenu(show: boolean) {
if (show && optionSelected) {
return;
}
optionSelected = false;
if (menuTimeout) {
clearTimeout(menuTimeout);
}
// Use a timeout to "debounce" the display of the menu.
menuTimeout = setTimeout(() => menuSurface.setOpen(show), 100);
}
});
function showMenu(show: boolean) {
if (show && optionSelected) {
return;
}
optionSelected = false;
if (menuTimeout) {
clearTimeout(menuTimeout);
}
// Use a timeout to "debounce" the display of the menu.
menuTimeout = setTimeout(() => menuSurface.setOpen(show), 100);
}
</script>
<div class="textfield-with-options">
<TextField
bind:value
on:focusin={() => showMenu(true)}
on:focusout={() => showMenu(false)}
use={[[virtualKeyboardChange, (newValue) => (value = newValue)]]}
{...$$restProps}
>
<div slot="trailingIcon">
{#if valid}
<Icon class="fa fa-check-circle-o" style="color: green;" />
{/if}
</div>
<HelperText persistent slot="helper">{helperText}</HelperText>
</TextField>
<TextField
bind:value
on:focusin={() => showMenu(true)}
on:focusout={() => showMenu(false)}
use={[[virtualKeyboardChange, (newValue) => (value = newValue)]]}
{...$$restProps}
>
<div slot="trailingIcon">
{#if valid}
<Icon class="fa fa-check-circle-o" style="color: green;" />
{/if}
</div>
<HelperText persistent slot="helper">{helperText}</HelperText>
</TextField>
<MenuSurface bind:this={menuSurface} anchorCorner="BOTTOM_LEFT">
<div style="display: flex; flex-direction: row;">
{#each options as group}
<List>
{#each group as option}
<Item
on:SMUI:action={() => {
value = option;
showMenu(false);
<MenuSurface bind:this={menuSurface} anchorCorner="BOTTOM_LEFT">
<div style="display: flex; flex-direction: row;">
{#each options as group}
<List>
{#each group as option}
<Item
on:SMUI:action={() => {
value = option;
showMenu(false);
optionSelected = true;
}}
>
<Text>{option}</Text>
</Item>
{/each}
</List>
{/each}
</div>
</MenuSurface>
optionSelected = true;
}}
>
<Text>{option}</Text>
</Item>
{/each}
</List>
{/each}
</div>
</MenuSurface>
</div>
<style lang="scss">
:global {
.textfield-with-options {
.mdc-deprecated-list-item {
height: 32px;
}
:global {
.textfield-with-options {
.mdc-deprecated-list-item {
height: 32px;
}
}
}
}
</style>

View File

@@ -1,95 +1,104 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import * as api from "$lib/api";
import { virtualKeyboardChange } from "$lib/CustomActions";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import * as api from "$lib/api";
import { virtualKeyboardChange } from "$lib/CustomActions";
// https://man7.org/linux/man-pages/man7/hostname.7.html
//
// Each element of the hostname must be from 1 to 63 characters long
// and the entire hostname, including the dots, can be at most 253
// characters long. Valid characters for hostnames are ASCII(7)
// letters from a to z, the digits from 0 to 9, and the hyphen (-).
// A hostname may not start with a hyphen.
// https://man7.org/linux/man-pages/man7/hostname.7.html
//
// Each element of the hostname must be from 1 to 63 characters long
// and the entire hostname, including the dots, can be at most 253
// characters long. Valid characters for hostnames are ASCII(7)
// letters from a to z, the digits from 0 to 9, and the hyphen (-).
// A hostname may not start with a hyphen.
const pattern = /[a-zA-Z0-9][a-zA-Z0-9-]{0,62}/;
const pattern = /[a-zA-Z0-9][a-zA-Z0-9-]{0,62}/;
export let open = false;
export let open = false;
let rebooting = false;
let redirectTimeout = 45;
let hostname = "";
let rebooting = false;
let redirectTimeout = 45;
let hostname = "";
$: setTimeout(() => {
hostname = (hostname.match(pattern) || [""])[0].toLowerCase();
}, 0);
$: setTimeout(() => {
hostname = (hostname.match(pattern) || [""])[0].toLowerCase();
}, 0);
$: if (open) {
hostname = "";
}
async function onConfirm() {
rebooting = true;
await api.PUT("hostname", { hostname });
await api.PUT("reboot");
const interval = setInterval(() => {
if (0 < redirectTimeout) {
redirectTimeout -= 1;
} else {
clearInterval(interval);
location.hostname = getRedirectTarget();
}
}, 1000);
}
function getRedirectTarget() {
if (location.hostname.endsWith(".local")) {
return `${hostname}.local`;
$: if (open) {
hostname = "";
}
if (location.hostname.endsWith(".lan")) {
return `${hostname}.lan`;
async function onConfirm() {
rebooting = true;
await api.PUT("hostname", { hostname });
await api.PUT("reboot");
const interval = setInterval(() => {
if (0 < redirectTimeout) {
redirectTimeout -= 1;
} else {
clearInterval(interval);
location.hostname = getRedirectTarget();
}
}, 1000);
}
return hostname;
}
function getRedirectTarget() {
if (location.hostname.endsWith(".local")) {
return `${hostname}.local`;
}
if (location.hostname.endsWith(".lan")) {
return `${hostname}.lan`;
}
return hostname;
}
</script>
<MessageDialog open={rebooting} title="Rebooting" noaction>
Rebooting to apply the hostname change...
Rebooting to apply the hostname change...
</MessageDialog>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="change-hostname-dialog-title"
aria-describedby="change-hostname-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="change-hostname-dialog-title"
aria-describedby="change-hostname-dialog-content"
>
<Title id="change-hostname-dialog-title">Change Hostname</Title>
<Title id="change-hostname-dialog-title">Change Hostname</Title>
<Content id="change-hostname-dialog-content">
<TextField
bind:value={hostname}
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (hostname = newValue)],
]}
label="New Hostname"
spellcheck="false"
variant="filled"
style="width: 100%;"
/>
</Content>
<Content id="change-hostname-dialog-content">
<TextField
bind:value={hostname}
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (hostname = newValue)],
]}
label="New Hostname"
spellcheck="false"
variant="filled"
style="width: 100%;"
/>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button defaultAction on:click={onConfirm} disabled={hostname.length === 0}>
<Label>Confirm & Reboot</Label>
</Button>
</Actions>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
on:click={onConfirm}
disabled={hostname.length === 0}
>
<Label>Confirm & Reboot</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -1,232 +1,235 @@
<script lang="ts" context="module">
import { writable } from "svelte/store";
import HomeMachineDialog from "$dialogs/HomeMachineDialog.svelte";
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import UploadDialog from "$dialogs/UploadDialog.svelte";
import SetTimeDialog from "./SetTimeDialog.svelte";
import ManualHomeAxisDialog from "./ManualHomeAxisDialog.svelte";
import SetAxisPositionDialog from "./SetAxisPositionDialog.svelte";
import MoveToZeroDialog from "./MoveToZeroDialog.svelte";
import ShutdownDialog from "./ShutdownDialog.svelte";
import MessageDialog from "./MessageDialog.svelte";
import { writable } from "svelte/store";
import HomeMachineDialog from "$dialogs/HomeMachineDialog.svelte";
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
import UploadDialog from "$dialogs/UploadDialog.svelte";
import SetTimeDialog from "./SetTimeDialog.svelte";
import ManualHomeAxisDialog from "./ManualHomeAxisDialog.svelte";
import SetAxisPositionDialog from "./SetAxisPositionDialog.svelte";
import MoveToZeroDialog from "./MoveToZeroDialog.svelte";
import ShutdownDialog from "./ShutdownDialog.svelte";
import MessageDialog from "./MessageDialog.svelte";
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
type HomeMachineDialogPropsType = {
open: boolean;
home: () => void;
};
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
type HomeMachineDialogPropsType = {
open: boolean;
home: () => void;
};
const ProbeDialogProps = writable<ProbeDialogPropsType>();
type ProbeDialogPropsType = {
open: boolean;
probeType: "xyz" | "z";
};
const ProbeDialogProps = writable<ProbeDialogPropsType>();
type ProbeDialogPropsType = {
open: boolean;
probeType: "xyz" | "z";
};
const ScreenRotationDialogProps = writable<ScreenRotationDialogPropsType>();
type ScreenRotationDialogPropsType = {
open: boolean;
};
const ScreenRotationDialogProps = writable<ScreenRotationDialogPropsType>();
type ScreenRotationDialogPropsType = {
open: boolean;
};
const UploadDialogProps = writable<UploadDialogPropsType>();
type UploadDialogPropsType = {
open: boolean;
file: File;
onComplete: () => void;
};
const UploadDialogProps = writable<UploadDialogPropsType>();
type UploadDialogPropsType = {
open: boolean;
file: File;
onComplete: () => void;
};
const SetTimeDialogProps = writable<SetTimeDialogPropsType>();
type SetTimeDialogPropsType = {
open: boolean;
};
const SetTimeDialogProps = writable<SetTimeDialogPropsType>();
type SetTimeDialogPropsType = {
open: boolean;
};
const ManualHomeAxisDialogProps = writable<ManualHomeAxisDialogPropsType>();
type ManualHomeAxisDialogPropsType = {
open: boolean;
axis: string;
};
const ManualHomeAxisDialogProps = writable<ManualHomeAxisDialogPropsType>();
type ManualHomeAxisDialogPropsType = {
open: boolean;
axis: string;
};
const SetAxisPositionDialogProps = writable<SetAxisPositionDialogPropsType>();
type SetAxisPositionDialogPropsType = {
open: boolean;
axis: string;
};
const SetAxisPositionDialogProps =
writable<SetAxisPositionDialogPropsType>();
type SetAxisPositionDialogPropsType = {
open: boolean;
axis: string;
};
const MoveToZeroDialogProps = writable<MoveToZeroDialogPropsType>();
type MoveToZeroDialogPropsType = {
open: boolean;
axes: "xy" | "z";
};
const MoveToZeroDialogProps = writable<MoveToZeroDialogPropsType>();
type MoveToZeroDialogPropsType = {
open: boolean;
axes: "xy" | "z";
};
const ShutdownDialogProps = writable<ShutdownDialogPropsType>();
type ShutdownDialogPropsType = {
open: boolean;
};
const ShutdownDialogProps = writable<ShutdownDialogPropsType>();
type ShutdownDialogPropsType = {
open: boolean;
};
const MessageDialogProps = writable<MessageDialogPropsType>();
type MessageDialogPropsType = {
open: boolean;
title: string;
message: string;
noaction: boolean;
};
const MessageDialogProps = writable<MessageDialogPropsType>();
type MessageDialogPropsType = {
open: boolean;
title: string;
message: string;
noaction: boolean;
};
export function showDialog(
dialog: "HomeMachine",
props: Omit<HomeMachineDialogPropsType, "open">
);
export function showDialog(
dialog: "HomeMachine",
props: Omit<HomeMachineDialogPropsType, "open">
);
export function showDialog(
dialog: "Probe",
props: Omit<ProbeDialogPropsType, "open">
);
export function showDialog(
dialog: "Probe",
props: Omit<ProbeDialogPropsType, "open">
);
export function showDialog(
dialog: "ScreenRotation",
props: Omit<ScreenRotationDialogPropsType, "open">
);
export function showDialog(
dialog: "ScreenRotation",
props: Omit<ScreenRotationDialogPropsType, "open">
);
export function showDialog(
dialog: "Upload",
props: Omit<UploadDialogPropsType, "open">
);
export function showDialog(
dialog: "Upload",
props: Omit<UploadDialogPropsType, "open">
);
export function showDialog(
dialog: "SetTime",
props: Omit<SetTimeDialogPropsType, "open">
);
export function showDialog(
dialog: "SetTime",
props: Omit<SetTimeDialogPropsType, "open">
);
export function showDialog(
dialog: "ManualHomeAxis",
props: Omit<ManualHomeAxisDialogPropsType, "open">
);
export function showDialog(
dialog: "ManualHomeAxis",
props: Omit<ManualHomeAxisDialogPropsType, "open">
);
export function showDialog(
dialog: "SetAxisPosition",
props: Omit<SetAxisPositionDialogPropsType, "open">
);
export function showDialog(
dialog: "SetAxisPosition",
props: Omit<SetAxisPositionDialogPropsType, "open">
);
export function showDialog(
dialog: "MoveToZero",
props: Omit<MoveToZeroDialogPropsType, "open">
);
export function showDialog(
dialog: "MoveToZero",
props: Omit<MoveToZeroDialogPropsType, "open">
);
export function showDialog(
dialog: "Shutdown",
props: Omit<ShutdownDialogPropsType, "open">
);
export function showDialog(
dialog: "Shutdown",
props: Omit<ShutdownDialogPropsType, "open">
);
export function showDialog(
dialog: "Message",
props: Omit<MessageDialogPropsType, "open">
);
export function showDialog(
dialog: "Message",
props: Omit<MessageDialogPropsType, "open">
);
export function showDialog(dialog: string, props: any) {
switch (dialog) {
case "HomeMachine":
HomeMachineDialogProps.set({ ...props, open: true });
break;
export function showDialog(dialog: string, props: any) {
switch (dialog) {
case "HomeMachine":
HomeMachineDialogProps.set({ ...props, open: true });
break;
case "Probe":
ProbeDialogProps.set({ ...props, open: true });
break;
case "Probe":
ProbeDialogProps.set({ ...props, open: true });
break;
case "ScreenRotation":
ScreenRotationDialogProps.set({ ...props, open: true });
break;
case "ScreenRotation":
ScreenRotationDialogProps.set({ ...props, open: true });
break;
case "Upload":
UploadDialogProps.set({ ...props, open: true });
break;
case "Upload":
UploadDialogProps.set({ ...props, open: true });
break;
case "SetTime":
SetTimeDialogProps.set({ ...props, open: true });
break;
case "SetTime":
SetTimeDialogProps.set({ ...props, open: true });
break;
case "ManualHomeAxis":
ManualHomeAxisDialogProps.set({ ...props, open: true });
break;
case "ManualHomeAxis":
ManualHomeAxisDialogProps.set({ ...props, open: true });
break;
case "SetAxisPosition":
SetAxisPositionDialogProps.set({ ...props, open: true });
break;
case "SetAxisPosition":
SetAxisPositionDialogProps.set({ ...props, open: true });
break;
case "MoveToZero":
MoveToZeroDialogProps.set({ ...props, open: true });
break;
case "MoveToZero":
MoveToZeroDialogProps.set({ ...props, open: true });
break;
case "Shutdown":
ShutdownDialogProps.set({ ...props, open: true });
break;
case "Shutdown":
ShutdownDialogProps.set({ ...props, open: true });
break;
case "Message":
MessageDialogProps.set({ ...props, open: true });
break;
case "Message":
MessageDialogProps.set({ ...props, open: true });
break;
default:
throw new Error(`Unknown dialog '${dialog}'`);
default:
throw new Error(`Unknown dialog '${dialog}'`);
}
}
}
</script>
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { onMount, onDestroy } from "svelte";
let bodyObserver: MutationObserver;
let keyboardObserver: MutationObserver;
let bodyObserver: MutationObserver;
let keyboardObserver: MutationObserver;
onMount(() => {
bodyObserver = new MutationObserver(() => {
const virtualKeyboard = document.getElementById(
"virtualKeyboardChromeExtension"
);
onMount(() => {
bodyObserver = new MutationObserver(() => {
const virtualKeyboard = document.getElementById(
"virtualKeyboardChromeExtension"
);
if (virtualKeyboard) {
bodyObserver.disconnect();
bodyObserver = undefined;
if (virtualKeyboard) {
bodyObserver.disconnect();
bodyObserver = undefined;
const virtualKeyboardOverlay = document.getElementById(
"virtualKeyboardChromeExtensionOverlayScrollExtend"
);
const virtualKeyboardOverlay = document.getElementById(
"virtualKeyboardChromeExtensionOverlayScrollExtend"
);
keyboardObserver = new MutationObserver(() => {
const open = virtualKeyboard.getAttribute("_state") === "open";
const keyboardHeight = Number.parseFloat(
virtualKeyboardOverlay.style.height
);
keyboardObserver = new MutationObserver(() => {
const open =
virtualKeyboard.getAttribute("_state") === "open";
const keyboardHeight = Number.parseFloat(
virtualKeyboardOverlay.style.height
);
const dialogContainers = document.querySelectorAll<HTMLDivElement>(
".mdc-dialog .mdc-dialog__container"
);
const dialogContainers =
document.querySelectorAll<HTMLDivElement>(
".mdc-dialog .mdc-dialog__container"
);
for (let dialogContainer of dialogContainers) {
dialogContainer.style["marginBottom"] = open
? `${keyboardHeight}px`
: "";
}
for (let dialogContainer of dialogContainers) {
dialogContainer.style["marginBottom"] = open
? `${keyboardHeight}px`
: "";
}
});
keyboardObserver.observe(virtualKeyboard, { attributes: true });
}
});
keyboardObserver.observe(virtualKeyboard, { attributes: true });
}
bodyObserver.observe(document.querySelector("body"), {
subtree: false,
childList: true,
});
});
bodyObserver.observe(document.querySelector("body"), {
subtree: false,
childList: true,
onDestroy(() => {
if (bodyObserver) {
bodyObserver.disconnect();
bodyObserver = undefined;
}
if (keyboardObserver) {
keyboardObserver.disconnect();
keyboardObserver = undefined;
}
});
});
onDestroy(() => {
if (bodyObserver) {
bodyObserver.disconnect();
bodyObserver = undefined;
}
if (keyboardObserver) {
keyboardObserver.disconnect();
keyboardObserver = undefined;
}
});
</script>
<HomeMachineDialog {...$HomeMachineDialogProps} />

View File

@@ -1,28 +1,33 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
export let open;
export let home: () => any;
export let open;
export let home: () => any;
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="home-machine-dialog-title"
aria-describedby="home-machine-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="home-machine-dialog-title"
aria-describedby="home-machine-dialog-content"
>
<Title id="home-machine-dialog-title">Home Machine</Title>
<Title id="home-machine-dialog-title">Home Machine</Title>
<Content id="home-machine-dialog-content">Home the machine?</Content>
<Content id="home-machine-dialog-content">Home the machine?</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button defaultAction use={[InitialFocus]} on:click={home}>
<Label>OK</Label>
</Button>
</Actions>
<Button defaultAction use={[InitialFocus]} on:click={home}>
<Label>OK</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -1,51 +1,57 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import TextField from "@smui/textfield";
import Button, { Label } from "@smui/button";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { virtualKeyboardChange } from "$lib/CustomActions";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import TextField from "@smui/textfield";
import Button, { Label } from "@smui/button";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open: boolean;
export let axis = "";
export let open: boolean;
export let axis = "";
let value = 0;
let value = 0;
function onConfirm() {
ControllerMethods.set_home(axis, value);
}
function onConfirm() {
ControllerMethods.set_home(axis, value);
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="manual-home-axis-dialog-title"
aria-describedby="manual-home-axis-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="manual-home-axis-dialog-title"
aria-describedby="manual-home-axis-dialog-content"
>
<Title id="manual-home-axis-dialog-title"
>Manually Home {axis.toUpperCase()} Axis</Title
>
<Content id="manual-home-axis-dialog-content">
<p>Set axis absolute position</p>
<Title id="manual-home-axis-dialog-title">
Manually Home {axis.toUpperCase()} Axis
</Title>
<TextField
label="Absolute"
type="number"
bind:value
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (value = newValue)],
]}
variant="filled"
style="width: 100%;"
/>
</Content>
<Content id="manual-home-axis-dialog-content">
<p>Set axis absolute position</p>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button defaultAction on:click={onConfirm}>
<Label>Set</Label>
</Button>
</Actions>
<TextField
label="Absolute"
type="number"
bind:value
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (value = newValue)],
]}
variant="filled"
style="width: 100%;"
/>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button defaultAction on:click={onConfirm}>
<Label>Set</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -1,31 +1,36 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
export let open: boolean;
export let title = "";
export let message = "";
export let noaction = false;
export let open: boolean;
export let title = "";
export let message = "";
export let noaction = false;
</script>
<Dialog
bind:open
scrimClickAction=""
escapeKeyAction=""
aria-labelledby="message-dialog-title"
aria-describedby="message-dialog-content"
bind:open
scrimClickAction=""
escapeKeyAction=""
aria-labelledby="message-dialog-title"
aria-describedby="message-dialog-content"
>
<Title id="message-dialog-title">{title}</Title>
<Title id="message-dialog-title">{title}</Title>
<Content id="message-dialog-content">
<slot>{message}</slot>
</Content>
<Content id="message-dialog-content">
<slot>{message}</slot>
</Content>
{#if !noaction}
<Actions>
<Button defaultAction use={[InitialFocus]}>
<Label>OK</Label>
</Button>
</Actions>
{/if}
{#if !noaction}
<Actions>
<Button defaultAction use={[InitialFocus]}>
<Label>OK</Label>
</Button>
</Actions>
{/if}
</Dialog>

View File

@@ -1,444 +1,455 @@
<script type="ts">
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { waitForChange } from "$lib/StoreHelpers";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { Config } from "$lib/ConfigStore";
import { writable, type Writable } from "svelte/store";
import {
probingActive,
probeContacted,
probingComplete,
probingFailed,
probingStarted,
} from "$lib/ControllerState";
import { numberWithUnit } from "$lib/RegexHelpers";
import TextFieldWithOptions from "$components/TextFieldWithOptions.svelte";
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { waitForChange } from "$lib/StoreHelpers";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { Config } from "$lib/ConfigStore";
import { writable, type Writable } from "svelte/store";
import {
probingActive,
probeContacted,
probingComplete,
probingFailed,
probingStarted,
} from "$lib/ControllerState";
import { numberWithUnit } from "$lib/RegexHelpers";
import TextFieldWithOptions from "$components/TextFieldWithOptions.svelte";
const ValidSteps = [
"None",
"CheckProbe",
"BitDimensions",
"PlaceProbeBlock",
"Probe",
"Done",
] as const;
type Step = typeof ValidSteps[number];
function isStep(str): str is Step {
return ValidSteps.includes(str);
}
const stepLabels: Record<Step, string> = {
None: "",
CheckProbe: "Check probe",
BitDimensions: "Bit dimensions",
PlaceProbeBlock: "Place probe block",
Probe: "Probe",
Done: "Done",
};
const cancelled = writable(false);
const userAcknowledged = writable(false);
const imperialBits: `${number}/${number} in`[] = [
"1/2 in",
"3/8 in",
"1/4 in",
"1/8 in",
"1/16 in",
"1/32 in",
];
const metricBits: `${number} mm`[] = [
"12 mm",
"10 mm",
"8 mm",
"6 mm",
"4 mm",
"3 mm",
];
export let open;
export let probeType: "xyz" | "z";
let currentStep: Step = "None";
let cutterDiameterString: string = "";
let cutterDiameterMetric: number;
let showCancelButton = true;
let steps: Step[] = [];
let nextButton = {
label: "Next",
disabled: false,
allowClose: false,
};
$: metric = $Config.settings?.units === "METRIC";
$: cutterDiameterMetric = numberWithUnit
.parse(cutterDiameterString)
?.toMetric();
$: if (open) {
cutterDiameterString = localStorage.getItem("cutterDiameter") ?? "";
// Svelte appears not to like it when you invoke
// an async function from a reactive statement, so we
// use requestAnimationFrame to call 'begin' at a later moment.
requestAnimationFrame(begin);
}
$: if (cutterDiameterString) {
updateButtons();
}
async function begin() {
try {
$probingActive = true;
assertValidProbeType();
steps = [
const ValidSteps = [
"None",
"CheckProbe",
probeType === "xyz" ? "BitDimensions" : undefined,
"BitDimensions",
"PlaceProbeBlock",
"Probe",
"Done",
].filter<Step>(isStep);
] as const;
await stepCompleted("CheckProbe", probeContacted);
type Step = typeof ValidSteps[number];
if (probeType === "xyz") {
await stepCompleted("BitDimensions", userAcknowledged);
localStorage.setItem(
"cutterDiameter",
numberWithUnit.normalize(cutterDiameterString)
);
}
await stepCompleted("PlaceProbeBlock", userAcknowledged);
await stepCompleted("Probe", probingComplete, probingFailed);
await stepCompleted("Done", userAcknowledged);
if (probeType === "xyz") {
ControllerMethods.gotoZero("xy");
}
} catch (err) {
if (err.message !== "cancelled") {
console.error("Error during probing:", err);
}
} finally {
$probingActive = false;
currentStep = "None";
if ($probingStarted) {
ControllerMethods.stop();
}
clearFlags();
}
}
function assertValidProbeType() {
switch (probeType) {
case "xyz":
case "z":
break;
default:
throw new Error(`Invalid probe type: ${probeType}`);
}
}
async function stepCompleted(
nextStep: Step,
...writables: Array<Writable<any>>
) {
currentStep = nextStep;
clearFlags();
updateButtons();
if (currentStep === "Probe") {
executeProbe();
function isStep(str): str is Step {
return ValidSteps.includes(str);
}
await Promise.race([
...writables.map((writable) => waitForChange(writable)),
waitForChange(cancelled),
]);
if ($cancelled) {
throw new Error("cancelled");
}
}
function clearFlags(foo: string = "") {
$cancelled = false;
$probeContacted = false;
$probingStarted = false;
$probingFailed = false;
$probingComplete = false;
$userAcknowledged = false;
}
function updateButtons() {
showCancelButton = true;
nextButton = {
label: "Next",
disabled: false,
allowClose: false,
const stepLabels: Record<Step, string> = {
None: "",
CheckProbe: "Check probe",
BitDimensions: "Bit dimensions",
PlaceProbeBlock: "Place probe block",
Probe: "Probe",
Done: "Done",
};
switch (currentStep) {
case "CheckProbe":
case "Probe":
nextButton.disabled = true;
break;
const cancelled = writable(false);
const userAcknowledged = writable(false);
case "BitDimensions":
nextButton.disabled = !isFinite(cutterDiameterMetric);
break;
const imperialBits: `${number}/${number} in`[] = [
"1/2 in",
"3/8 in",
"1/4 in",
"1/8 in",
"1/16 in",
"1/32 in",
];
const metricBits: `${number} mm`[] = [
"12 mm",
"10 mm",
"8 mm",
"6 mm",
"4 mm",
"3 mm",
];
export let open;
export let probeType: "xyz" | "z";
let currentStep: Step = "None";
let cutterDiameterString: string = "";
let cutterDiameterMetric: number;
let showCancelButton = true;
let steps: Step[] = [];
let nextButton = {
label: "Next",
disabled: false,
allowClose: false,
};
$: metric = $Config.settings?.units === "METRIC";
$: cutterDiameterMetric = numberWithUnit
.parse(cutterDiameterString)
?.toMetric();
$: if (open) {
cutterDiameterString = localStorage.getItem("cutterDiameter") ?? "";
// Svelte appears not to like it when you invoke
// an async function from a reactive statement, so we
// use requestAnimationFrame to call 'begin' at a later moment.
requestAnimationFrame(begin);
}
$: if (cutterDiameterString) {
updateButtons();
}
async function begin() {
try {
$probingActive = true;
assertValidProbeType();
steps = [
"CheckProbe",
probeType === "xyz" ? "BitDimensions" : undefined,
"PlaceProbeBlock",
"Probe",
"Done",
].filter<Step>(isStep);
await stepCompleted("CheckProbe", probeContacted);
if (probeType === "xyz") {
await stepCompleted("BitDimensions", userAcknowledged);
localStorage.setItem(
"cutterDiameter",
numberWithUnit.normalize(cutterDiameterString)
);
}
await stepCompleted("PlaceProbeBlock", userAcknowledged);
await stepCompleted("Probe", probingComplete, probingFailed);
await stepCompleted("Done", userAcknowledged);
if (probeType === "xyz") {
ControllerMethods.gotoZero("xy");
}
} catch (err) {
if (err.message !== "cancelled") {
console.error("Error during probing:", err);
}
} finally {
$probingActive = false;
currentStep = "None";
if ($probingStarted) {
ControllerMethods.stop();
}
clearFlags();
}
}
function assertValidProbeType() {
switch (probeType) {
case "xyz":
case "z":
break;
default:
throw new Error(`Invalid probe type: ${probeType}`);
}
}
async function stepCompleted(
nextStep: Step,
...writables: Array<Writable<any>>
) {
currentStep = nextStep;
clearFlags();
updateButtons();
if (currentStep === "Probe") {
executeProbe();
}
await Promise.race([
...writables.map((writable) => waitForChange(writable)),
waitForChange(cancelled),
]);
if ($cancelled) {
throw new Error("cancelled");
}
}
function clearFlags(foo: string = "") {
$cancelled = false;
$probeContacted = false;
$probingStarted = false;
$probingFailed = false;
$probingComplete = false;
$userAcknowledged = false;
}
function updateButtons() {
showCancelButton = true;
case "Done":
showCancelButton = false;
nextButton = {
disabled: false,
label: "Done",
allowClose: true,
label: "Next",
disabled: false,
allowClose: false,
};
break;
switch (currentStep) {
case "CheckProbe":
case "Probe":
nextButton.disabled = true;
break;
case "BitDimensions":
nextButton.disabled = !isFinite(cutterDiameterMetric);
break;
case "Done":
showCancelButton = false;
nextButton = {
disabled: false,
label: "Done",
allowClose: true,
};
break;
}
}
}
function executeProbe() {
const probeBlockWidth = $Config.probe["probe-xdim"];
const probeBlockLength = $Config.probe["probe-ydim"];
const probeBlockHeight = $Config.probe["probe-zdim"];
const slowSeek = $Config.probe["probe-slow-seek"];
const fastSeek = $Config.probe["probe-fast-seek"];
function executeProbe() {
const probeBlockWidth = $Config.probe["probe-xdim"];
const probeBlockLength = $Config.probe["probe-ydim"];
const probeBlockHeight = $Config.probe["probe-zdim"];
const slowSeek = $Config.probe["probe-slow-seek"];
const fastSeek = $Config.probe["probe-fast-seek"];
const cutterLength = 12.7;
const zLift = 1;
const xOffset = probeBlockWidth + cutterDiameterMetric / 2.0;
const yOffset = probeBlockLength + cutterDiameterMetric / 2.0;
const zOffset = probeBlockHeight;
const cutterLength = 12.7;
const zLift = 1;
const xOffset = probeBlockWidth + cutterDiameterMetric / 2.0;
const yOffset = probeBlockLength + cutterDiameterMetric / 2.0;
const zOffset = probeBlockHeight;
if (probeType === "z") {
ControllerMethods.send(`
G21
G92 Z0
G38.2 Z -25.4 F${fastSeek}
G91 G1 Z 1
G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset}
G91 G0 Z 3
if (probeType === "z") {
ControllerMethods.send(`
G21
G92 Z0
G38.2 Z -25.4 F${fastSeek}
G91 G1 Z 1
G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset}
G91 G0 Z 3
M2
`);
} else {
// After probing Z, we want to drop the bit down:
// Ideally, 12.7mm/0.5in
// And we don't want to be more than 90% down on the probe block
// Also, add zlift to compensate for the fact that we lift after probing Z
const plunge = Math.min(cutterLength, zOffset * 0.9) + zLift;
M2
`);
} else {
// After probing Z, we want to drop the bit down:
// Ideally, 12.7mm/0.5in
// And we don't want to be more than 90% down on the probe block
// Also, add zlift to compensate for the fact that we lift after probing Z
const plunge = Math.min(cutterLength, zOffset * 0.9) + zLift;
ControllerMethods.send(`
G21
G92 X0 Y0 Z0
G38.2 Z -25 F${fastSeek}
G91 G1 Z 1
G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset}
G91 G0 Z ${zLift}
G91 G0 X 20
G91 G0 Z ${-plunge}
G38.2 X -20 F${fastSeek}
G91 G1 X 1
G38.2 X -2 F${slowSeek}
G92 X ${xOffset}
ControllerMethods.send(`
G21
G92 X0 Y0 Z0
G38.2 Z -25 F${fastSeek}
G91 G1 Z 1
G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset}
G91 G0 Z ${zLift}
G91 G0 X 20
G91 G0 Z ${-plunge}
G38.2 X -20 F${fastSeek}
G91 G1 X 1
G38.2 X -2 F${slowSeek}
G92 X ${xOffset}
G91 G0 X 1
G91 G0 Y 20
G91 G0 X -20
G38.2 Y -20 F${fastSeek}
G91 G1 Y 1
G38.2 Y -2 F${slowSeek}
G92 Y ${yOffset}
G91 G0 X 1
G91 G0 Y 20
G91 G0 X -20
G38.2 Y -20 F${fastSeek}
G91 G1 Y 1
G38.2 Y -2 F${slowSeek}
G92 Y ${yOffset}
G91 G0 Y 3
G91 G0 Z 25
G91 G0 Y 3
G91 G0 Z 25
M2
`);
M2
`);
}
}
}
</script>
<Dialog
bind:open
class="probe-dialog"
scrimClickAction=""
aria-labelledby="probe-dialog-title"
aria-describedby="probe-dialog-content"
surface$style="width: 700px; max-width: calc(100vw - 32px);"
bind:open
class="probe-dialog"
scrimClickAction=""
aria-labelledby="probe-dialog-title"
aria-describedby="probe-dialog-content"
surface$style="width: 700px; max-width: calc(100vw - 32px);"
>
<Title id="probe-dialog-title">Probing {probeType?.toUpperCase()}</Title>
<Title id="probe-dialog-title">Probing {probeType?.toUpperCase()}</Title>
<Content id="probe-dialog-content" style="overflow: visible;">
<div class="steps">
<p><b>Step {steps.indexOf(currentStep) + 1} of {steps.length}</b></p>
<ul>
{#each steps as step}
<li class:active={currentStep === step}>{stepLabels[step]}</li>
{/each}
</ul>
</div>
<p>
{#if currentStep === "CheckProbe"}
Attach the probe magnet to the collet, then touch the probe block to the
bit.
{:else if currentStep === "BitDimensions"}
<TextFieldWithOptions
label="Cutter diameter"
variant="filled"
spellcheck="false"
style="width: 100%;"
bind:value={cutterDiameterString}
options={[imperialBits, metricBits]}
valid={isFinite(cutterDiameterMetric)}
helperText={`Examples: 1/2", 10 mm, 0.25 in`}
/>
{:else if currentStep === "PlaceProbeBlock"}
{#if probeType === "xyz"}
Place the probe block face up, on the lower-left corner of your
workpiece.
{:else}
Place the probe block face down, with the bit above the recess.
<Content id="probe-dialog-content" style="overflow: visible;">
<div class="steps">
<p>
<b>Step {steps.indexOf(currentStep) + 1} of {steps.length}</b>
</p>
<ul>
{#each steps as step}
<li class:active={currentStep === step}>
{stepLabels[step]}
</li>
{/each}
</ul>
</div>
<p>
{#if currentStep === "CheckProbe"}
Attach the probe magnet to the collet, then touch the probe
block to the bit.
{:else if currentStep === "BitDimensions"}
<TextFieldWithOptions
label="Cutter diameter"
variant="filled"
spellcheck="false"
style="width: 100%;"
bind:value={cutterDiameterString}
options={[imperialBits, metricBits]}
valid={isFinite(cutterDiameterMetric)}
helperText={`Examples: 1/2", 10 mm, 0.25 in`}
/>
{:else if currentStep === "PlaceProbeBlock"}
{#if probeType === "xyz"}
Place the probe block face up, on the lower-left corner of
your workpiece.
{:else}
Place the probe block face down, with the bit above the
recess.
{/if}
<p>
The probing procedure will begin as soon as you click
'Next'.
</p>
{:else if currentStep === "Probe"}
Probing in progress...
{:else if currentStep === "Done"}
{#if $probingFailed}
Could not find the probe block during probing!
<p>
Make sure the tip of the bit is less than {metric
? "25mm"
: "1 in"}
above the probe block, and try again.
</p>
{:else}
Don't forget to put away the probe!
{#if probeType === "xyz"}
<p>The machine will now move to the XY origin.</p>
<p>Watch your hands!</p>
{/if}
{/if}
{/if}
</p>
</Content>
<Actions>
{#if showCancelButton}
<Button on:click={() => ($cancelled = true)}>
<Label>Cancel</Label>
</Button>
{/if}
<p>The probing procedure will begin as soon as you click 'Next'.</p>
{:else if currentStep === "Probe"}
Probing in progress...
{:else if currentStep === "Done"}
{#if $probingFailed}
Could not find the probe block during probing!
<p>
Make sure the tip of the bit is less than {metric ? "25mm" : "1 in"}
above the probe block, and try again.
</p>
{:else}
Don't forget to put away the probe!
{#if probeType === "xyz"}
<p>The machine will now move to the XY origin.</p>
<p>Watch your hands!</p>
{/if}
{/if}
{/if}
</p>
</Content>
<Actions>
{#if showCancelButton}
<Button on:click={() => ($cancelled = true)}>
<Label>Cancel</Label>
</Button>
{/if}
<Button
defaultAction
data-mdc-dialog-action={nextButton.allowClose ? "close" : ""}
disabled={nextButton.disabled}
on:click={() => ($userAcknowledged = true)}
>
<Label>
{nextButton.label}
</Label>
</Button>
</Actions>
<Button
defaultAction
data-mdc-dialog-action={nextButton.allowClose ? "close" : ""}
disabled={nextButton.disabled}
on:click={() => ($userAcknowledged = true)}
>
<Label>
{nextButton.label}
</Label>
</Button>
</Actions>
</Dialog>
<style lang="scss">
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
:global {
#probe-dialog-content {
display: flex;
flex-direction: row;
:global {
#probe-dialog-content {
display: flex;
flex-direction: row;
}
.bit-dimensions {
display: flex;
flex-direction: column;
}
.steps {
margin-right: 50px;
ul {
margin: 0 auto;
list-style-type: none;
counter-reset: steps;
margin: 0;
font-family: sans-serif;
padding-inline-start: 20px;
}
ul li {
padding: 0 0 12px 30px;
position: relative;
margin: 0;
white-space: nowrap;
color: $text;
&:after {
position: absolute;
top: 3px;
left: 0.5px;
content: "";
border: 2px solid $text;
border-radius: 50%;
display: inline-block;
height: 11px;
width: 11px;
text-align: center;
line-height: 12px;
background: transparent;
}
&:before {
position: absolute;
left: 7px;
top: 22px;
bottom: 0;
content: "";
width: 0;
border-left: 2px solid $text;
}
&:last-of-type:before {
border: none;
}
&.active {
color: $primary;
font-weight: bold;
&:after {
border: 3px solid $primary;
top: 2.5px;
left: 0;
}
}
}
}
}
.bit-dimensions {
display: flex;
flex-direction: column;
}
.steps {
margin-right: 50px;
ul {
margin: 0 auto;
list-style-type: none;
counter-reset: steps;
margin: 0;
font-family: sans-serif;
padding-inline-start: 20px;
}
ul li {
padding: 0 0 12px 30px;
position: relative;
margin: 0;
white-space: nowrap;
color: $text;
&:after {
position: absolute;
top: 3px;
left: 0.5px;
content: "";
border: 2px solid $text;
border-radius: 50%;
display: inline-block;
height: 11px;
width: 11px;
text-align: center;
line-height: 12px;
background: transparent;
}
&:before {
position: absolute;
left: 7px;
top: 22px;
bottom: 0;
content: "";
width: 0;
border-left: 2px solid $text;
}
&:last-of-type:before {
border: none;
}
&.active {
color: $primary;
font-weight: bold;
&:after {
border: 3px solid $primary;
top: 2.5px;
left: 0;
}
}
}
}
}
</style>

View File

@@ -1,76 +1,81 @@
<script type="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import Radio from "@smui/radio";
import FormField from "@smui/form-field";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import * as Api from "$lib/api";
import { onMount } from "svelte";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
import Radio from "@smui/radio";
import FormField from "@smui/form-field";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import * as Api from "$lib/api";
import { onMount } from "svelte";
const options = [
{ value: 0, label: "Normal" },
{ value: 1, label: "Upside-down" },
];
const options = [
{ value: 0, label: "Normal" },
{ value: 1, label: "Upside-down" },
];
export let open;
let currentValue;
let value;
let rebooting;
export let open;
let currentValue;
let value;
let rebooting;
onMount(async () => {
const result = await Api.GET("screen-rotation");
currentValue = value = result.rotated ? 1 : 0;
});
onMount(async () => {
const result = await Api.GET("screen-rotation");
currentValue = value = result.rotated ? 1 : 0;
});
async function onConfirm() {
rebooting = true;
async function onConfirm() {
rebooting = true;
await Api.PUT("screen-rotation", { rotated: value === 1 });
}
await Api.PUT("screen-rotation", { rotated: value === 1 });
}
</script>
<MessageDialog open={rebooting} title="Rebooting" noaction>
Rebooting to apply the new screen rotation...
Rebooting to apply the new screen rotation...
</MessageDialog>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="screen-rotation-dialog-title"
aria-describedby="screen-rotation-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="screen-rotation-dialog-title"
aria-describedby="screen-rotation-dialog-content"
>
<Title id="screen-rotation-dialog-title">Screen Rotation</Title>
<Title id="screen-rotation-dialog-title">Screen Rotation</Title>
<Content id="screen-rotation-dialog-content">
{#each options as option}
<FormField>
<Radio bind:group={value} value={option.value} />
<span slot="label">
{option.label}
</span>
</FormField>
{/each}
</Content>
<Content id="screen-rotation-dialog-content">
{#each options as option}
<FormField>
<Radio bind:group={value} value={option.value} />
<span slot="label">
{option.label}
</span>
</FormField>
{/each}
</Content>
<Actions>
<Button use={[InitialFocus]}>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
disabled={value === currentValue}
on:click={onConfirm}
>
<Label>Confirm & Reboot</Label>
</Button>
</Actions>
<Actions>
<Button use={[InitialFocus]}>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
disabled={value === currentValue}
on:click={onConfirm}
>
<Label>Confirm & Reboot</Label>
</Button>
</Actions>
</Dialog>
<style lang="scss">
:global {
#screen-rotation-dialog-content {
display: flex;
flex-direction: column;
:global {
#screen-rotation-dialog-content {
display: flex;
flex-direction: column;
}
}
}
</style>

View File

@@ -1,69 +1,75 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import TextField from "@smui/textfield";
import Button, { Label } from "@smui/button";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { virtualKeyboardChange } from "$lib/CustomActions";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import TextField from "@smui/textfield";
import Button, { Label } from "@smui/button";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open: boolean;
export let axis = "";
export let open: boolean;
export let axis = "";
let value = 0;
let homed = false;
let wasOpen = false;
let value = 0;
let homed = false;
let wasOpen = false;
$: if (open != wasOpen) {
if (open) {
homed = ControllerMethods.isAxisHomed(axis);
$: if (open != wasOpen) {
if (open) {
homed = ControllerMethods.isAxisHomed(axis);
}
wasOpen = open;
}
wasOpen = open;
}
function onUnhome() {
ControllerMethods.unhome(axis);
}
function onUnhome() {
ControllerMethods.unhome(axis);
}
function onConfirm() {
ControllerMethods.set_position(axis, value);
}
function onConfirm() {
ControllerMethods.set_position(axis, value);
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="set-axis-position-dialog-title"
aria-describedby="set-axis-position-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="set-axis-position-dialog-title"
aria-describedby="set-axis-position-dialog-content"
>
<Title id="set-axis-position-dialog-title"
>Set {axis.toUpperCase()} Axis Position</Title
>
<Content id="set-axis-position-dialog-content">
<TextField
label="Position"
type="number"
bind:value
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (value = newValue)],
]}
spellcheck="false"
variant="filled"
style="width: 100%;"
/>
</Content>
<Title id="set-axis-position-dialog-title">
Set {axis.toUpperCase()} Axis Position
</Title>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
{#if homed}
<Button on:click={onUnhome}>
<Label>Unhome</Label>
</Button>
{/if}
<Button defaultAction on:click={onConfirm}>
<Label>Set</Label>
</Button>
</Actions>
<Content id="set-axis-position-dialog-content">
<TextField
label="Position"
type="number"
bind:value
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (value = newValue)],
]}
spellcheck="false"
variant="filled"
style="width: 100%;"
/>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
{#if homed}
<Button on:click={onUnhome}>
<Label>Unhome</Label>
</Button>
{/if}
<Button defaultAction on:click={onConfirm}>
<Label>Set</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -1,245 +1,256 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import CircularProgress from "@smui/circular-progress";
import VirtualList from "svelte-tiny-virtual-list";
import * as api from "$lib/api";
import { virtualKeyboardChange } from "$lib/CustomActions";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import CircularProgress from "@smui/circular-progress";
import VirtualList from "svelte-tiny-virtual-list";
import * as api from "$lib/api";
import { virtualKeyboardChange } from "$lib/CustomActions";
const itemHeight = 35;
const itemHeight = 35;
type Timezone = {
label: string;
value: string;
};
type Timezone = {
label: string;
value: string;
};
export let open = false;
let value = "";
let wasOpen = false;
let loading = true;
let timezones: Timezone[] = [];
let currentTimezoneIndex: number;
let selectedTimezoneIndex: number;
let networkTimeSynchronized: boolean;
export let open = false;
let value = "";
let wasOpen = false;
let loading = true;
let timezones: Timezone[] = [];
let currentTimezoneIndex: number;
let selectedTimezoneIndex: number;
let networkTimeSynchronized: boolean;
$: if (open != wasOpen) {
if (!wasOpen) {
loadData();
$: if (open != wasOpen) {
if (!wasOpen) {
loadData();
}
wasOpen = open;
}
wasOpen = open;
}
async function loadData() {
loading = true;
async function loadData() {
loading = true;
const result = await api.GET("time");
const result = await api.GET("time");
parseTimezones(result.timezones);
parseTimeinfo(result.timeinfo);
value = getDateTimeValueString();
parseTimezones(result.timezones);
parseTimeinfo(result.timeinfo);
value = getDateTimeValueString();
loading = false;
}
function parseTimeinfo(str: string) {
const matches = Array.from(str.matchAll(/\s*([^:]+):\s+(.+)/gm));
let currentTimezoneValue;
for (const match of matches) {
let [, label, value] = match;
switch (label) {
case "Time zone":
currentTimezoneValue = value.split(" ")[0];
break;
case "NTP synchronized":
networkTimeSynchronized = value === "yes";
break;
}
loading = false;
}
currentTimezoneIndex = timezones.findIndex(
(tz) => tz.value === currentTimezoneValue
);
selectedTimezoneIndex = currentTimezoneIndex;
}
function parseTimeinfo(str: string) {
const matches = Array.from(str.matchAll(/\s*([^:]+):\s+(.+)/gm));
function parseTimezones(str: string) {
const matches = Array.from(str.matchAll(/\s*(\S+)\s*/gm));
let currentTimezoneValue;
for (const match of matches) {
let [, label, value] = match;
timezones = [];
for (let [, value] of matches) {
timezones.push({
label: value.replace(/_/g, " "),
value,
});
switch (label) {
case "Time zone":
currentTimezoneValue = value.split(" ")[0];
break;
case "NTP synchronized":
networkTimeSynchronized = value === "yes";
break;
}
}
currentTimezoneIndex = timezones.findIndex(
(tz) => tz.value === currentTimezoneValue
);
selectedTimezoneIndex = currentTimezoneIndex;
}
// Sort alphabetically, but with the current timezone at the top of the list
timezones.sort((a, b) => {
switch (true) {
case a.value === "UTC":
return -1;
function parseTimezones(str: string) {
const matches = Array.from(str.matchAll(/\s*(\S+)\s*/gm));
case b.value === "UTC":
return 1;
timezones = [];
for (let [, value] of matches) {
timezones.push({
label: value.replace(/_/g, " "),
value,
});
}
default:
return a.value.localeCompare(b.value);
}
});
}
// Sort alphabetically, but with the current timezone at the top of the list
timezones.sort((a, b) => {
switch (true) {
case a.value === "UTC":
return -1;
function getDateTimeValueString() {
const date = new Date();
case b.value === "UTC":
return 1;
const year = date.getFullYear().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
default:
return a.value.localeCompare(b.value);
}
});
}
return `${year}-${month}-${day}T${hour}:${minute}:00`;
}
function getDateTimeValueString() {
const date = new Date();
async function onConfirm() {
const date = new Date(value);
const year = date.getFullYear().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
const year = date.getFullYear().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
await api.PUT("time", {
datetime: `${year}-${month}-${day} ${hour}:${minute}:00`,
timezone: timezones[selectedTimezoneIndex].value,
});
}
return `${year}-${month}-${day}T${hour}:${minute}:00`;
}
async function onConfirm() {
const date = new Date(value);
const year = date.getFullYear().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
await api.PUT("time", {
datetime: `${year}-${month}-${day} ${hour}:${minute}:00`,
timezone: timezones[selectedTimezoneIndex].value,
});
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="set-time-dialog-title"
aria-describedby="set-time-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="set-time-dialog-title"
aria-describedby="set-time-dialog-content"
>
<Title id="set-time-dialog-title">Adjust Clock & Timezone</Title>
<Title id="set-time-dialog-title">Adjust Clock & Timezone</Title>
<Content id="set-time-dialog-content">
{#if loading}
<div style="display: flex; justify-content: center">
<CircularProgress style="height: 32px; width: 32px;" indeterminate />
</div>
{:else}
{#if networkTimeSynchronized}
<p>
Because this controller is connected to the internet, the time is
managed automatically, and cannot be manually set.
</p>
{:else}
<p>
Because this controller is not connected to the internet, you can
manually set the time.
</p>
<Content id="set-time-dialog-content">
{#if loading}
<div style="display: flex; justify-content: center">
<CircularProgress
style="height: 32px; width: 32px;"
indeterminate
/>
</div>
{:else}
{#if networkTimeSynchronized}
<p>
Because this controller is connected to the internet, the
time is managed automatically, and cannot be manually set.
</p>
{:else}
<p>
Because this controller is not connected to the internet,
you can manually set the time.
</p>
<p>
Note: any time the controller is turned off, the time will need to be
reset. If you connect the controller to the internet, the time will be
managed automatically.
</p>
<p>
Note: any time the controller is turned off, the time will
need to be reset. If you connect the controller to the
internet, the time will be managed automatically.
</p>
<Label>Date & Time</Label>
<TextField
bind:value
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (value = newValue)],
]}
label="Time"
type="datetime-local"
variant="filled"
style="width: 100%;"
/>
{/if}
<Label>Date & Time</Label>
<TextField
bind:value
use={[
InitialFocus,
[
virtualKeyboardChange,
(newValue) => (value = newValue),
],
]}
label="Time"
type="datetime-local"
variant="filled"
style="width: 100%;"
/>
{/if}
<p>
To display your local time correctly, the controller must know what
timezone it is in.
</p>
<p>
To display your local time correctly, the controller must know
what timezone it is in.
</p>
<div class="timezones-container" style="margin-top: 20px;">
<Label>Select your timezone</Label>
<VirtualList
width="100%"
height={itemHeight * 6}
itemCount={timezones.length}
itemSize={itemHeight}
scrollToIndex={currentTimezoneIndex}
scrollToAlignment="center"
<div class="timezones-container" style="margin-top: 20px;">
<Label>Select your timezone</Label>
<VirtualList
width="100%"
height={itemHeight * 6}
itemCount={timezones.length}
itemSize={itemHeight}
scrollToIndex={currentTimezoneIndex}
scrollToAlignment="center"
>
<div
slot="item"
let:index
let:style
{style}
class="timezone"
class:selected={index === selectedTimezoneIndex}
on:click={() => (selectedTimezoneIndex = index)}
>
{timezones[index].label}
</div>
</VirtualList>
</div>
{/if}
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
disabled={selectedTimezoneIndex === -1}
on:click={onConfirm}
>
<div
slot="item"
let:index
let:style
{style}
class="timezone"
class:selected={index === selectedTimezoneIndex}
on:click={() => (selectedTimezoneIndex = index)}
>
{timezones[index].label}
</div>
</VirtualList>
</div>
{/if}
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
disabled={selectedTimezoneIndex === -1}
on:click={onConfirm}
>
<Label>Confirm</Label>
</Button>
</Actions>
<Label>Confirm</Label>
</Button>
</Actions>
</Dialog>
<style lang="scss">
@use "sass:color";
@use "sass:color";
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
$primary: #0078e7;
$very-dark: #555;
$text: #777;
$grey: #bbb;
$light: #ddd;
.timezones-container {
:global {
.virtual-list-wrapper {
border: 1px solid #ccc;
border-radius: 3px;
overflow-x: hidden;
overflow-y: scroll;
}
.timezones-container {
:global {
.virtual-list-wrapper {
border: 1px solid #ccc;
border-radius: 3px;
overflow-x: hidden;
overflow-y: scroll;
}
}
.timezone {
font-size: 14px;
display: flex;
align-items: center;
margin: 0;
padding-left: 10px;
&.selected {
color: $primary;
background-color: color.adjust($primary, $lightness: 50%);
font-weight: bold;
}
}
}
.timezone {
font-size: 14px;
display: flex;
align-items: center;
margin: 0;
padding-left: 10px;
&.selected {
color: $primary;
background-color: color.adjust($primary, $lightness: 50%);
font-weight: bold;
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Dialog, { Title, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import * as Api from "$lib/api"
import * as Api from "$lib/api";
export let open;

View File

@@ -1,78 +1,83 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import LinearProgress from "@smui/linear-progress";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
import LinearProgress from "@smui/linear-progress";
export let open = false;
export let file: File;
export let onComplete: () => void;
export let open = false;
export let file: File;
export let onComplete: () => void;
let wasOpen = false;
let xhr;
let progress;
let wasOpen = false;
let xhr;
let progress;
$: if (open != wasOpen) {
if (!wasOpen) {
beginUpload();
$: if (open != wasOpen) {
if (!wasOpen) {
beginUpload();
}
wasOpen = open;
}
wasOpen = open;
}
$: if (!open) {
xhr = undefined;
}
$: if (!open) {
xhr = undefined;
}
async function beginUpload() {
progress = 0;
async function beginUpload() {
progress = 0;
xhr = new XMLHttpRequest();
xhr.upload.onload = () => {
open = false;
if (onComplete) {
onComplete();
}
};
xhr = new XMLHttpRequest();
xhr.upload.onload = () => {
open = false;
if (onComplete) {
onComplete();
}
};
xhr.upload.onerror = () => {
open = false;
alert("Upload failed.");
};
xhr.upload.onerror = () => {
open = false;
alert("Upload failed.");
};
xhr.upload.onabort = () => {
open = false;
};
xhr.upload.onabort = () => {
open = false;
};
xhr.upload.onprogress = (event) => {
progress = event.loaded / event.total;
};
xhr.upload.onprogress = (event) => {
progress = event.loaded / event.total;
};
xhr.open("PUT", `/api/file/${encodeURIComponent(file.name)}`);
xhr.send(file);
}
xhr.open("PUT", `/api/file/${encodeURIComponent(file.name)}`);
xhr.send(file);
}
function onCancel() {
xhr.abort();
}
function onCancel() {
xhr.abort();
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="upload-dialog-title"
aria-describedby="upload-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="upload-dialog-title"
aria-describedby="upload-dialog-content"
>
<Title id="upload-dialog-title">
Uploading {#if file}{file.name}...{/if}
</Title>
<Title id="upload-dialog-title">
Uploading {#if file}{file.name}...{/if}
</Title>
<Content id="upload-dialog-content">
<LinearProgress {progress} />
</Content>
<Content id="upload-dialog-content">
<LinearProgress {progress} />
</Content>
<Actions>
<Button on:click={onCancel} use={[InitialFocus]}>
<Label>Cancel</Label>
</Button>
</Actions>
<Actions>
<Button on:click={onCancel} use={[InitialFocus]}>
<Label>Cancel</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -1,103 +1,115 @@
<script lang="ts">
import Dialog, { Title, Content, Actions, InitialFocus } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import Icon from "@smui/textfield/icon";
import HelperText from "@smui/textfield/helper-text";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import type { WifiNetwork } from "$lib/NetworkInfo";
import * as api from "$lib/api";
import { virtualKeyboardChange } from "$lib/CustomActions";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
import TextField from "@smui/textfield";
import Icon from "@smui/textfield/icon";
import HelperText from "@smui/textfield/helper-text";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import type { WifiNetwork } from "$lib/NetworkInfo";
import * as api from "$lib/api";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open = false;
export let network: WifiNetwork;
export let open = false;
export let network: WifiNetwork;
let rebooting = false;
let password = "";
let showPassword = false;
let rebooting = false;
let password = "";
let showPassword = false;
$: needPassword = !network?.active && network?.Encryption !== "Open";
$: connectOrDisconnect = network?.active ? "Disconnect" : "Connect";
$: connectToOrDisconnectFrom = network?.active
? "Disconnect from"
: "Connect to";
$: needPassword = !network?.active && network?.Encryption !== "Open";
$: connectOrDisconnect = network?.active ? "Disconnect" : "Connect";
$: connectToOrDisconnectFrom = network?.active
? "Disconnect from"
: "Connect to";
$: if (open) {
password = "";
}
$: if (open) {
password = "";
}
async function onConfirm() {
rebooting = true;
async function onConfirm() {
rebooting = true;
await api.PUT("network", {
wifi: {
enabled: !network.active,
ssid: network.Name,
password,
},
});
}
await api.PUT("network", {
wifi: {
enabled: !network.active,
ssid: network.Name,
password,
},
});
}
</script>
<MessageDialog open={rebooting} title="Rebooting" noaction>
Rebooting to apply Wifi changes...
Rebooting to apply Wifi changes...
</MessageDialog>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="wifi-connection-dialog-title"
aria-describedby="wifi-connection-dialog-content"
bind:open
scrimClickAction=""
aria-labelledby="wifi-connection-dialog-title"
aria-describedby="wifi-connection-dialog-content"
>
<Title id="wifi-connection-dialog-title">
{connectToOrDisconnectFrom}
{network.Name}
</Title>
<Title id="wifi-connection-dialog-title">
{connectToOrDisconnectFrom}
{network.Name}
</Title>
<Content id="wifi-connection-dialog-content">
{#if needPassword}
<TextField
bind:value={password}
use={[
InitialFocus,
[virtualKeyboardChange, (newValue) => (password = newValue)],
]}
label="Password"
spellcheck="false"
variant="filled"
type={showPassword ? "text" : "password"}
style="width: 100%;"
>
<div
slot="trailingIcon"
on:click={() => (showPassword = !showPassword)}
<Content id="wifi-connection-dialog-content">
{#if needPassword}
<TextField
bind:value={password}
use={[
InitialFocus,
[
virtualKeyboardChange,
(newValue) => (password = newValue),
],
]}
label="Password"
spellcheck="false"
variant="filled"
type={showPassword ? "text" : "password"}
style="width: 100%;"
>
<div
slot="trailingIcon"
on:click={() => (showPassword = !showPassword)}
>
<Icon
class={`fa ${showPassword ? "fa-eye-slash" : "fa-eye"}`}
/>
</div>
<HelperText persistent slot="helper">
Wifi passwords must be 8 to 128 characters
</HelperText>
</TextField>
{/if}
<p>
<em>
Clicking {connectOrDisconnect} will reboot the controller to apply
the changes.
</em>
</p>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
on:click={onConfirm}
disabled={needPassword &&
(password.length < 8 || password.length > 128)}
>
<Icon class={`fa ${showPassword ? "fa-eye-slash" : "fa-eye"}`} />
</div>
<HelperText persistent slot="helper">
Wifi passwords must be 8 to 128 characters
</HelperText>
</TextField>
{/if}
<p>
<em>
Clicking {connectOrDisconnect} will reboot the controller to apply the changes.
</em>
</p>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
on:click={onConfirm}
disabled={needPassword && (password.length < 8 || password.length > 128)}
>
<Label>{connectOrDisconnect} & Reboot</Label>
</Button>
</Actions>
<Label>{connectOrDisconnect} & Reboot</Label>
</Button>
</Actions>
</Dialog>

View File

@@ -31,15 +31,15 @@ const empty: NetworkInfo = {
ssid: "",
networks: []
}
}
};
export const networkInfo = writable<NetworkInfo>(empty);
export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
const now = Date.now();
const networksByName: Record<string, WifiNetwork> = {}
const networksByName: Record<string, WifiNetwork> = {};
for (let network of rawNetworkInfo.wifi.networks) {
for (const network of rawNetworkInfo.wifi.networks) {
if (network.Name) {
network.lastSeen = now;
network.active = rawNetworkInfo.wifi.ssid === network.Name;
@@ -53,7 +53,7 @@ export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
}
}
for (let network of Object.values(networksByName)) {
for (const network of Object.values(networksByName)) {
if (network.lastSeen - now > 30000) {
delete networksByName[network.Name];
}

View File

@@ -1,9 +1,3 @@
type NumberWithUnit = {
value: number,
metric: boolean,
toMetric: () => number;
}
function numberWithUnitToMetric() {
return this.metric ? this.value : this.value * 25.4;
}
@@ -38,12 +32,13 @@ function formatFraction(value: number) {
export const numberWithUnit = {
regex: /^\s*(?:(\d+)\s*\/\s*(\d+)|(\d*\.\d+)|(\d+(?:\.\d+)?))\s*("|in|inch|inches|mm|millimeters)\s*$/,
parse: function (str: string) {
// eslint-disable-next-line prefer-const
let [, numerator, denominator, decimal1, decimal2, unit]: any = str?.match(numberWithUnit.regex) ?? [];
numerator = Number.parseFloat(numerator)
denominator = Number.parseFloat(denominator)
decimal1 = Number.parseFloat(decimal1)
decimal2 = Number.parseFloat(decimal2)
numerator = Number.parseFloat(numerator);
denominator = Number.parseFloat(denominator);
decimal1 = Number.parseFloat(decimal1);
decimal2 = Number.parseFloat(decimal2);
const metric = (unit ?? "").includes("m");
@@ -81,10 +76,10 @@ export const numberWithUnit = {
return "";
case value.metric:
return `${value.value} mm`
return `${value.value} mm`;
default:
return `${formatFraction(value.value)} in`
return `${formatFraction(value.value)} in`;
}
}
}
};

View File

@@ -6,36 +6,36 @@ async function doFetch(method: HttpMethod, url: string, data: any, config: Reque
...config,
method,
cache: "no-cache",
body: (typeof data === 'object')
body: (typeof data === "object")
? JSON.stringify(data)
: undefined,
headers: (typeof data === 'object')
headers: (typeof data === "object")
? {
"Content-Type": 'application/json; charset=utf-8'
"Content-Type": "application/json; charset=utf-8"
}
: {}
});
return await response.json();
} catch (error) {
console.debug('API Error: ' + url + ': ' + error);
console.debug(`API Error: ${url}: ${error}`);
throw error;
}
}
export async function GET(url: string, config: RequestInit = {}) {
return doFetch('GET', url, undefined, config);
return doFetch("GET", url, undefined, config);
}
export async function PUT(url: string, data: any = undefined, config: RequestInit = {}) {
return doFetch('PUT', url, data, config);
return doFetch("PUT", url, data, config);
}
export async function POST(url: string, data: any = undefined, config: RequestInit = {}) {
return doFetch('POST', url, data, config);
return doFetch("POST", url, data, config);
}
export async function DELETE(url: string, config = {}) {
return doFetch('DELETE', url, undefined, config);
return doFetch("DELETE", url, undefined, config);
}

View File

@@ -1,35 +1,35 @@
import 'polyfill-object.fromentries';
import "polyfill-object.fromentries";
import matchAll from "string.prototype.matchall";
matchAll.shim();
import AdminNetworkView from '$components/AdminNetworkView.svelte';
import SettingsView from '$components/SettingsView.svelte';
import AdminNetworkView from "$components/AdminNetworkView.svelte";
import SettingsView from "$components/SettingsView.svelte";
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
import { handleConfigUpdate, setDisplayUnits } from '$lib/ConfigStore';
import { handleConfigUpdate, setDisplayUnits } from "$lib/ConfigStore";
import { handleControllerStateUpdate } from "$lib/ControllerState";
import { registerControllerMethods } from "$lib/RegisterControllerMethods";
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
switch (component) {
case "AdminNetworkView":
return new AdminNetworkView({ target, props });
switch (component) {
case "AdminNetworkView":
return new AdminNetworkView({ target, props });
case "SettingsView":
return new SettingsView({ target, props });
case "SettingsView":
return new SettingsView({ target, props });
case "DialogHost":
return new DialogHost({ target, props });
case "DialogHost":
return new DialogHost({ target, props });
default:
throw new Error("Unknown component");
}
default:
throw new Error("Unknown component");
}
}
export {
showDialog,
handleControllerStateUpdate,
handleConfigUpdate,
registerControllerMethods,
setDisplayUnits
showDialog,
handleControllerStateUpdate,
handleConfigUpdate,
registerControllerMethods,
setDisplayUnits
};