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" "postinstall": "cd src/svelte-components && npm i"
}, },
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"browserify": "^17.0.0", "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", "jshint": "^2.13.4",
"jstransformer-escape-html": "^1.1.0", "jstransformer-escape-html": "^1.1.0",
"jstransformer-scss": "^2.0.0", "jstransformer-scss": "^2.0.0",
@@ -16,4 +23,4 @@
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"pug-cli": "^1.0.0-alpha6" "pug-cli": "^1.0.0-alpha6"
} }
} }

View File

@@ -1,143 +1,123 @@
/******************************************************************************\ "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 merge = require("lodash.merge"); const merge = require("lodash.merge");
const config_defaults = require("../resources/onefinity_defaults.json"); const config_defaults = require("../resources/onefinity_defaults.json");
const variant_defaults = { const variant_defaults = {
machinist_x35: require("../resources/onefinity_machinist_x35_defaults.json"), machinist_x35: require("../resources/onefinity_machinist_x35_defaults.json"),
woodworker_x35: require("../resources/onefinity_woodworker_x35_defaults.json"), woodworker_x35: require("../resources/onefinity_woodworker_x35_defaults.json"),
woodworker_x50: require("../resources/onefinity_woodworker_x50_defaults.json"), woodworker_x50: require("../resources/onefinity_woodworker_x50_defaults.json"),
journeyman_x50: require("../resources/onefinity_journeyman_x50_defaults.json") journeyman_x50: require("../resources/onefinity_journeyman_x50_defaults.json")
}; };
const api = require('./api');
module.exports = { module.exports = {
template: '#admin-general-view-template', template: "#admin-general-view-template",
props: ['config', 'state'], props: ["config", "state"],
data: function () { data: function () {
return { return {
confirmReset: false, confirmReset: false,
autoCheckUpgrade: true, autoCheckUpgrade: true,
reset_variant: '' reset_variant: ""
} };
},
ready: function () {
this.autoCheckUpgrade = this.config.admin['auto-check-upgrade']
},
methods: {
backup: function () {
document.getElementById('download-target').src = '/api/config/download';
}, },
restore_config: function () { ready: function () {
// If we don't reset the form the browser may cache file if name is same this.autoCheckUpgrade = this.config.admin["auto-check-upgrade"];
// even if contents have changed
$('.restore-config')[0].reset();
$('.restore-config input').click();
}, },
restore: function (e) { methods: {
const files = e.target.files || e.dataTransfer.files; backup: function () {
if (!files.length) { document.getElementById("download-target").src = "/api/config/download";
return; },
}
const fileReader = new FileReader(); restore_config: function () {
fileReader.onload = async ({ target }) => { // If we don't reset the form the browser may cache file if name is same
let config; // even if contents have changed
try { $(".restore-config")[0].reset();
config = JSON.parse(target.result); $(".restore-config input").click();
} catch (ex) { },
api.alert("Invalid config file");
return; 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 = { module.exports = {
template: "#admin-network-view-template", template: "#admin-network-view-template",
attached: function () { attached: function () {
this.svelteComponent = SvelteComponents.createComponent( this.svelteComponent = SvelteComponents.createComponent(
"AdminNetworkView", "AdminNetworkView",
document.getElementById("admin-network") document.getElementById("admin-network")
); );
}, },
detached: function() { detached: function() {
this.svelteComponent.$destroy(); 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) { if (response.ok) {
config = $.extend({ return response.json();
type: method, }
url: '/api/' + url,
dataType: 'json',
cache: false
}, config);
if (typeof data == 'object') { throw new Error(await response.text());
config.data = JSON.stringify(data); } catch (error) {
config.contentType = 'application/json; charset=utf-8'; console.debug(`API Error: ${url}: ${error}`);
}
var d = $.Deferred(); throw error;
}
$.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();
} }
module.exports = { module.exports = {
get: function (url, config) { get: function (url) {
return api_cb('GET', url, undefined, config); return callApi("GET", url);
}, },
put: function(url, body = undefined) {
return callApi("PUT", url, body);
},
put: function(url, data, config) { delete: function (url) {
return api_cb('PUT', url, data, config); return callApi("DELETE", url);
},
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);
} }
};
alert(msg);
}
}

View File

@@ -5,411 +5,437 @@ const cookie = require("./cookie")("bbctrl-");
const Sock = require("./sock"); const Sock = require("./sock");
SvelteComponents.createComponent("DialogHost", SvelteComponents.createComponent("DialogHost",
document.getElementById("svelte-dialog-host") document.getElementById("svelte-dialog-host")
); );
function is_newer_version(current, latest) { function is_newer_version(current, latest) {
const pattern = /(\d+)\.(\d+)\.(\d+)(.*)/; const pattern = /(\d+)\.(\d+)\.(\d+)(.*)/;
const currentParts = current.match(pattern); const currentParts = current.match(pattern);
const latestParts = latest.match(pattern); const latestParts = latest.match(pattern);
if (!currentParts || !latestParts) { if (!currentParts || !latestParts) {
return false; return false;
} }
// Normal version comparisons // Normal version comparisons
const major = latestParts[1] - currentParts[1]; const major = latestParts[1] - currentParts[1];
const minor = latestParts[2] - currentParts[2]; const minor = latestParts[2] - currentParts[2];
const patch = latestParts[3] - currentParts[3]; const patch = latestParts[3] - currentParts[3];
// If current is a pre-release, and latest is a release // If current is a pre-release, and latest is a release
const betaToRelease = const betaToRelease = latestParts[4].length === 0 && currentParts[4].length > 0;
latestParts[4].length === 0 && currentParts[4].length > 0;
switch (true) { switch (true) {
case major > 0: case major > 0:
case major === 0 && minor > 0: case major === 0 && minor > 0:
case major === 0 && minor === 0 && patch > 0: case major === 0 && minor === 0 && patch > 0:
case major === 0 && minor === 0 && patch === 0 && betaToRelease: case major === 0 && minor === 0 && patch === 0 && betaToRelease:
return true; return true;
default: default:
return false; return false;
} }
} }
function is_object(o) { function is_object(o) {
return o !== null && typeof o == "object"; return o !== null && typeof o == "object";
} }
function is_array(o) { function is_array(o) {
return Array.isArray(o); return Array.isArray(o);
} }
function update_array(dst, src) { function update_array(dst, src) {
while (dst.length) dst.pop(); while (dst.length) {
for (var i = 0; i < src.length; i++) Vue.set(dst, i, src[i]); 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) { function update_object(dst, src, remove) {
var props, index, key, value; let props, index, key, value;
if (remove) { if (remove) {
props = Object.getOwnPropertyNames(dst); props = Object.getOwnPropertyNames(dst);
for (index in props) { for (index in props) {
key = props[index]; key = props[index];
if (!src.hasOwnProperty(key)) Vue.delete(dst, key); if (!hasOwnProperty(src, key)) {
Vue.delete(dst, key);
}
}
} }
}
props = Object.getOwnPropertyNames(src); props = Object.getOwnPropertyNames(src);
for (index in props) { for (index in props) {
key = props[index]; key = props[index];
value = src[key]; value = src[key];
if (is_array(value) && dst.hasOwnProperty(key) && is_array(dst[key])) if (is_array(value) && hasOwnProperty(dst, key) && is_array(dst[key])) {
update_array(dst[key], value); update_array(dst[key], value);
else if (is_object(value) && dst.hasOwnProperty(key) && is_object(dst[key])) } else if (is_object(value) && hasOwnProperty(dst, key) && is_object(dst[key])) {
update_object(dst[key], value, remove); update_object(dst[key], value, remove);
else Vue.set(dst, key, value); } else {
} Vue.set(dst, key, value);
}
}
} }
module.exports = new Vue({ module.exports = new Vue({
el: "body", el: "body",
data: function () { 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 () {
return { 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) { components: {
if (this.status == "connected") { estop: { template: "#estop-template" },
this.sock.send(msg); "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 () { watch: {
this.update(); display_units: function (value) {
localStorage.setItem("display_units", value);
SvelteComponents.setDisplayUnits(value);
},
}, },
update: function () { events: {
this.update(); "config-changed": function () {
}, this.modified = true;
},
check: async function () { send: function (msg) {
try { if (this.status == "connected") {
const response = await fetch("https://raw.githubusercontent.com/OneFinityCNC/onefinity-release/master/latest.txt", { this.sock.send(msg);
cache: "no-cache" }
}); },
this.latestVersion = (await response.text()).trim(); connected: function () {
} catch (err) { this.update();
this.latestVersion = ""; },
}
},
upgrade: function () { update: function () {
this.confirmUpgrade = true; this.update();
}, },
upload: function (firmware) { check: async function () {
this.firmware = firmware; try {
this.firmwareName = firmware.name; const response = await fetch("https://raw.githubusercontent.com/OneFinityCNC/onefinity-release/master/latest.txt", {
this.confirmUpload = true; cache: "no-cache"
}, });
error: function (msg) { this.latestVersion = (await response.text()).trim();
// Honor user error blocking } catch (err) {
if (Date.now() - this.errorTimeoutStart < this.errorTimeout * 1000) this.latestVersion = "";
return; }
},
// Wait at least 1 sec to pop up repeated errors upgrade: function () {
if (1 < msg.repeat && Date.now() - msg.ts < 1000) { this.confirmUpgrade = true;
return; },
}
// Popup error dialog upload: function (firmware) {
this.errorShow = true; this.firmware = firmware;
this.errorMessage = msg.msg; this.firmwareName = firmware.name;
}, this.confirmUpload = true;
}, },
computed: { error: function (msg) {
popupMessages: function () { // Honor user error blocking
const msgs = []; if (Date.now() - this.errorTimeoutStart < this.errorTimeout * 1000) {
return;
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;
} }
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); // Popup error dialog
this.errorShow = true;
SvelteComponents.handleControllerStateUpdate(this.state); this.errorMessage = msg.msg;
},
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 () { computed: {
var hash = location.hash.substr(1); popupMessages: function () {
const msgs = [];
if (!hash.trim().length) { for (let i = 0; i < this.state.messages.length; i++) {
location.hash = "control"; const text = this.state.messages[i].text;
return; if (!/^#/.test(text)) {
} msgs.push(text);
}
}
var parts = hash.split(":"); return msgs;
},
if (parts.length == 2) this.index = parts[1];
this.currentView = parts[0];
}, },
save: async function () { ready: function () {
const selected_tool = this.config.tool["selected-tool"]; $(window).on("hashchange", this.parse_hash);
const saveModbus = this.connect();
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; SvelteComponents.registerControllerMethods({
dispatch: (...args) => this.$dispatch(...args)
try { });
await api.put("config/save", this.config);
this.modified = false;
} catch (error) {
api.alert("Save failed", error);
}
}, },
close_messages: function (action) { methods: {
if (action == "stop") api.put("stop"); block_error_dialog: function () {
if (action == "continue") api.put("unpause"); this.errorTimeoutStart = Date.now();
this.errorShow = false;
},
// Acknowledge messages toggle_video: function () {
if (this.state.messages.length) { if (this.video_size == "small") {
var id = this.state.messages.slice(-1)[0].id; this.video_size = "large";
api.put("message/" + id + "/ack"); } 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 @@
/******************************************************************************\ "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'
module.exports = { module.exports = {
template: '#axis-control-template', template: "#axis-control-template",
props: ['axes', 'colors', 'enabled', 'adjust', 'step'], 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: { back2zero: function(axis0,axis1) {
jog: function (axis, ring, direction) { this.$dispatch("back2zero",this.axes[axis0],this.axes[axis1]);
var value = direction * this.value(ring); },
this.$dispatch(this.step ? 'step' : 'jog', this.axes[axis], value);
},
back2zero: function(axis0,axis1) { release: function (axis) {
this.$dispatch('back2zero',this.axes[axis0],this.axes[axis1]) 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) { text: function (ring) {
if (!this.step) this.$dispatch('jog', this.axes[axis], 0) let value = this.value(ring) * (this.step ? 1 : 100);
}, value = parseFloat(value.toFixed(3));
return value + (this.step ? "" : "%");
}
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 ? '' : '%');
} }
} };
}

View File

@@ -1,194 +1,254 @@
'use strict' "use strict";
module.exports = { module.exports = {
props: ['state', 'config'], props: ["state", "config"],
computed: { computed: {
metric: function () { this.$root.display_units === "METRIC" }, metric: function () {
x: function () { return this._compute_axis('x') }, return this.$root.display_units === "METRIC";
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() }
},
methods: { x: function () {
_convert_length: function (value) { return this._compute_axis("x");
return this.metric },
? value
: value / 25.4;
},
_length_str: function (value) { y: function () {
return this._convert_length(value).toLocaleString() + return this._compute_axis("y");
(this.metric ? ' mm' : ' in'); },
},
_compute_axis: function (axis) { z: function () {
var abs = this.state[axis + 'p'] || 0; return this._compute_axis("z");
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;
if (fault || shutdown) { a: function () {
state = shutdown ? 'SHUTDOWN' : 'FAULT'; return this._compute_axis("a");
klass += ' error'; },
icon = 'exclamation-circle';
} else if (homed) {
state = 'HOMED';
icon = 'check-circle';
}
if (0 < dim && dim < pathDim) { b: function () {
tstate = 'NO FIT'; return this._compute_axis("b");
tklass += ' error'; },
ticon = 'ban';
} else { c: function () {
if (over || under) { return this._compute_axis("c");
tstate = over ? 'OVER' : 'UNDER'; },
tklass += ' warn';
ticon = 'exclamation-circle'; axes: function () {
} else { return this._compute_axes();
tstate = 'OK';
ticon = 'check-circle';
} }
}
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) { methods: {
for (var i = 0; i < this.config.motors.length; i++) { _convert_length: function (value) {
var motor = this.config.motors[i]; return this.metric
if (motor.axis.toLowerCase() == axis) return i; ? value
} : value / 25.4;
},
return -1; _length_str: function (value) {
}, return this._convert_length(value).toLocaleString() + (this.metric ? " mm" : " in");
},
_compute_axes: function () { _compute_axis: function (axis) {
var homed = false; 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') { if (fault || shutdown) {
var axis = this[name]; state = shutdown ? "SHUTDOWN" : "FAULT";
klass += " error";
icon = "exclamation-circle";
} else if (homed) {
state = "HOMED";
icon = "check-circle";
}
if (!axis.enabled) continue if (0 < dim && dim < pathDim) {
if (!axis.homed) { homed = false; break } tstate = "NO FIT";
homed = true; 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; switch (state) {
var warn = false; case "UNHOMED":
title = "Click the home button to home axis.";
break;
if (homed) case "HOMED":
for (name of 'xyzabc') { title = "Axis successfuly homed.";
axis = this[name]; break;
if (!axis.enabled) continue; case "FAULT":
if (axis.klass.indexOf('error') != -1) error = true; title = [
if (axis.klass.indexOf('warn') != -1) warn = true; `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 @@
/******************************************************************************\ "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'
function _msg_equal(a, b) { function _msg_equal(a, b) {
return a.level == b.level && a.source == b.source && a.where == b.where && return a.level == b.level
a.msg == b.msg; && a.source == b.source
&& a.where == b.where
&&a.msg == b.msg;
} }
// Shared among all instances // Shared among all instances
var messages = []; const messages = [];
module.exports = { module.exports = {
template: '#console-template', template: "#console-template",
data: function () {
return {
messages
};
},
data: function () { events: {
return {messages: messages} 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: { // Make sure we have a message level
log: function (msg) { msg.level = msg.level || "info";
// 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 // Add to message log and count and collapse repeats
msg.level = msg.level || 'info'; 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 // Write message to browser console for debugging
var repeat = messages.length && _msg_equal(msg, messages[0]); const text = JSON.stringify(msg);
if (repeat) messages[0].repeat++; if (msg.level == "error" || msg.level == "critical") {
else { console.error(text);
msg.repeat = msg.repeat || 1; } else if (msg.level == "warning") {
messages.unshift(msg); console.warn(text);
while (256 < messages.length) messages.pop(); } else if (msg.level == "debug" && console.debug) {
} console.debug(text);
msg.ts = Date.now(); } else {
console.log(text);
}
// Write message to browser console for debugging // Event on errors
var text = JSON.stringify(msg); if (msg.level == "error" || msg.level == "critical") {
if (msg.level == 'error' || msg.level == 'critical') console.error(text); this.$dispatch("error", msg);
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 methods: {
if (msg.level == 'error' || msg.level == 'critical') clear: function () {
this.$dispatch('error', msg); 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'); const api = require("./api");
var cookie = require('./cookie')('bbctrl-'); const cookie = require("./cookie")("bbctrl-");
module.exports = { module.exports = {
template: '#control-view-template', template: "#control-view-template",
props: ['config', 'template', 'state'], props: ["config", "template", "state"],
data: function () { data: function () {
return { return {
current_time: "", current_time: "",
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL", mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
mdi: '', mdi: "",
last_file: undefined, last_file: undefined,
last_file_time: undefined, last_file_time: undefined,
toolpath: {}, toolpath: {},
toolpath_progress: 0, toolpath_progress: 0,
axes: 'xyzabc', axes: "xyzabc",
history: [], history: [],
speed_override: 1, speed_override: 1,
feed_override: 1, feed_override: 1,
jog_incr_amounts: { jog_incr_amounts: {
"METRIC": { "METRIC": {
fine: 0.1, fine: 0.1,
small: 1.0, small: 1.0,
medium: 10, medium: 10,
large: 100, 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: { "state.metric": {
'axis-control': require('./axis-control'), handler: function (metric) {
'path-viewer': require('./path-viewer'), this.mach_units = metric
'gcode-viewer': require('./gcode-viewer') ? "METRIC"
}, : "IMPERIAL";
},
immediate: true
},
watch: { "state.line": function () {
jog_incr: function (value) { if (this.mach_state != "HOMING") {
localStorage.setItem("jog_incr", value); this.$broadcast("gcode-line", this.state.line);
},
'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.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 () { computed: {
this.send(this.mdi); 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) { metric: function () {
this.history.unshift(this.mdi); 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 (state != "ESTOPPED" && (cycle == "jogging" || cycle == "homing")) {
if (this.state.xx == 'RUNNING') { return cycle.toUpperCase();
this.pause(); }
} else if (this.state.xx == 'STOPPING' || this.state.xx == 'HOLDING') {
this.unpause();
} else {
this.submit_mdi();
}
},
load_history: function (index) { return state || "";
this.mdi = this.history[index]; },
},
open: function (e) { pause_reason: function () {
// If we don't reset the form the browser may cache file if name is same return this.state.pr;
// even if contents have changed },
$('.gcode-file-input')[0].reset();
$('.gcode-file-input input').click();
},
upload: async function (e) { is_running: function () {
const files = e.target.files || e.dataTransfer.files; return this.mach_state == "RUNNING" || this.mach_state == "HOMING";
if (!files.length) { },
return;
}
const file = files[0]; is_stopping: function () {
return this.mach_state == "STOPPING";
},
const extension = file.name.split(".").pop(); is_holding: function () {
switch (extension.toLowerCase()) { return this.mach_state == "HOLDING";
case "nc": },
case "ngc":
case "gcode":
case "gc":
break;
default: is_ready: function () {
alert(`Unsupported file type: ${extension}`); return this.mach_state == "READY";
return; },
}
SvelteComponents.showDialog("Upload", { is_idle: function () {
file, return this.state.cycle == "idle";
onComplete: () => { },
this.last_file_time = undefined; // Force reload
this.$broadcast('gcode-reload', file.name); 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 () { events: {
if (this.state.selected) { jog: function (axis, power) {
api.delete('file/' + this.state.selected); 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 () { ready: function () {
api.delete('file'); this.load();
this.deleteGCode = false;
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) { methods: {
this.ask_home = false; getJogIncrStyle(value) {
const weight = `font-weight:${this.jog_incr === value ? "bold" : "normal"}`;
const color = this.jog_incr === value ? "color:#0078e7" : "";
if (typeof axis == 'undefined') { return [weight, color].join(";");
api.put('home'); },
} else if (this[axis].homingMode != 'manual') {
api.put('home/' + axis);
} else {
SvelteComponents.showDialog("ManualHomeAxis", { axis });
}
},
set_home: function (axis, position) { jog_fn: function (x_jog, y_jog, z_jog, a_jog) {
api.put('home/' + axis + '/set', { position: parseFloat(position) }); const amount = this.jog_incr_amounts[this.display_units][this.jog_incr];
},
unhome: function (axis) { const xcmd = `X${x_jog * amount}`;
api.put('home/' + axis + '/clear'); const ycmd = `Y${y_jog * amount}`;
}, const zcmd = `Z${z_jog * amount}`;
const acmd = `A${a_jog * amount}`;
show_set_position: function (axis) { this.send(`
SvelteComponents.showDialog("SetAxisPosition", { axis }); G91
}, ${this.metric ? "G21" : "G20"}
G0 ${xcmd}${ycmd}${zcmd}${acmd}
`);
},
showMoveToZeroDialog: function (axes) { send: function (msg) {
SvelteComponents.showDialog("MoveToZero", { axes }); this.$dispatch("send", msg);
}, },
showToolpathMessageDialog: function (axis) { load: function () {
SvelteComponents.showDialog("Message", { title: this[axis].toolmsg }); 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) { this.last_file = file;
api.put('position/' + axis, { 'position': parseFloat(position) }); this.last_file_time = file_time;
},
zero_all: function () { this.$broadcast("gcode-load", file);
for (var axis of 'xyzabc') { this.$broadcast("gcode-line", this.state.line);
if (this[axis].enabled) { this.toolpath_progress = 0;
this.zero(axis); 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) { mixins: [require("./axis-vars")]
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')]
}

View File

@@ -1,69 +1,49 @@
/******************************************************************************\ "use strict";
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'
module.exports = function (prefix) { module.exports = function (prefix) {
if (typeof prefix == 'undefined') 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';
} }
}
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 @@
/******************************************************************************\ "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'
var api = require('./api');
var entityMap = {
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
'/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;'}
const entityMap = {
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
"/": "&#x2F;", "`": "&#x60;", "=": "&#x3D;"};
function escapeHTML(s) { function escapeHTML(s) {
return s.replace(/[&<>"'`=\/]/g, function (c) {return entityMap[c]}) return s.replace(/[&<>"'`=\\/]/g, function (c) {
return entityMap[c];
});
} }
module.exports = { module.exports = {
template: '#gcode-viewer-template', template: "#gcode-viewer-template",
data: function () {
data: function () { return {
return { empty: true,
empty: true, file: "",
file: '', line: -1,
line: -1, scrolling: false
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);
}, },
events: {
clear: function () { "gcode-load": function (file) {
this.empty = true; this.load(file);
this.file = ''; },
this.line = -1; "gcode-clear": function () {
this.clusterize.clear(); this.clear();
}, },
"gcode-reload": function (file) {
this.reload(file);
reload: function (file) { },
if (file != this.file) return; "gcode-line": function (line) {
this.clear(); this.update_line(line);
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)
})
} }
} },
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 @@
/******************************************************************************\ "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'
var modbus = require('./modbus.js');
const modbus = require("./modbus.js");
module.exports = { module.exports = {
template: '#indicators-template', template: "#indicators-template",
props: ['state'], props: ["state"],
computed: {
modbus_status: function () {
return modbus.status_to_string(this.state.mx);
},
computed: { sense_error: function () {
modbus_status: function () {return modbus.status_to_string(this.state.mx)}, 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 () { return error;
var error = ''; }
},
if (this.state.motor_voltage_sense_error) error += 'Motor voltage\n'; methods: {
if (this.state.motor_current_sense_error) error += 'Motor current\n'; is_motor_enabled: function (motor) {
if (this.state.load1_sense_error) error += 'Load 1\n'; return typeof this.state[`${motor}me`] != "undefined" && this.state[`${motor}me`];
if (this.state.load2_sense_error) error += 'Load 2\n'; },
if (this.state.vdd_current_sense_error) error += 'Vdd current\n';
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 @@
/******************************************************************************\ "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'
module.exports = { module.exports = {
template: "#io-indicator-template", template: "#io-indicator-template",
props: ['name', 'state'], 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: { tooltip: function () {
klass: function () { switch (this.name) {
if (this.name == 'min-switch-0') return this.get_motor_min_class(0); case "min-switch-0": return this.get_motor_min_tooltip(0);
if (this.name == 'min-switch-1') return this.get_motor_min_class(1); case "min-switch-1": return this.get_motor_min_tooltip(1);
if (this.name == 'min-switch-2') return this.get_motor_min_class(2); case "min-switch-2": return this.get_motor_min_tooltip(2);
if (this.name == 'min-switch-3') return this.get_motor_min_class(3); case "min-switch-3": return this.get_motor_min_tooltip(3);
if (this.name == 'max-switch-0') return this.get_motor_max_class(0); case "max-switch-0": return this.get_motor_max_tooltip(0);
if (this.name == 'max-switch-1') return this.get_motor_max_class(1); case "max-switch-1": return this.get_motor_max_tooltip(1);
if (this.name == 'max-switch-2') return this.get_motor_max_class(2); case "max-switch-2": return this.get_motor_max_tooltip(2);
if (this.name == 'max-switch-3') return this.get_motor_max_class(3); case "max-switch-3": return this.get_motor_max_tooltip(3);
if (this.name == 'estop') return this.get_input_class('ew', 'et'); case "estop": return this.get_input_tooltip("ew", "et");
if (this.name == 'probe') return this.get_input_class('pw', 'pt'); case "probe": return this.get_input_tooltip("pw", "pt");
if (this.name == 'load-1') return this.get_output_class('1'); case "load-1": return this.get_output_tooltip("1");
if (this.name == 'load-2') return this.get_output_class('2'); case "load-2": return this.get_output_tooltip("2");
if (this.name == 'fault') return this.get_output_class('f'); case "fault": return this.get_output_tooltip("f");
if (this.name == 'tool-enable-mode') return this.get_output_class('e'); case "tool-direction-mode": return this.get_output_tooltip("d");
if (this.name == 'tool-direction-mode') return this.get_output_class('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 (state == 2) {
if (this.name == 'min-switch-0') return this.get_motor_min_tooltip(0); return "fa-circle-o";
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); const icon = state ? "fa-plus-circle" : "fa-minus-circle";
if (this.name == 'max-switch-0') return this.get_motor_max_tooltip(0); return `${icon} ${active ? "active" : "inactive"}`;
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); get_input_active: function (stateCode, typeCode) {
if (this.name == 'estop') return this.get_input_tooltip('ew', 'et'); const type = this.state[typeCode];
if (this.name == 'probe') return this.get_input_tooltip('pw', 'pt'); const state = this.state[stateCode];
if (this.name == 'load-1') return this.get_output_tooltip('1');
if (this.name == 'load-2') return this.get_output_tooltip('2'); if (type == 1) {
if (this.name == 'fault') return this.get_output_tooltip('f'); return !state; // Normally open
if (this.name == 'tool-direction-mode') } else if (type == 2) {
return this.get_output_tooltip('d'); return state; // Normally closed
if (this.name == 'tool-enable-mode') }
return this.get_output_tooltip('e');
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 @@
/******************************************************************************\ "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'
module.exports = { module.exports = {
template: '#io-view-template', template: "#io-view-template",
props: ['config', 'template', 'state'], props: ["config", "template", "state"],
events: {
events: { "input-changed": function() {
'input-changed': function() { this.$dispatch("config-changed");
this.$dispatch('config-changed'); return false;
return false; }
} }
} };
}

View File

@@ -1,148 +1,148 @@
'use strict'; "use strict";
function cookie_get(name) { function cookie_get(name) {
var decodedCookie = decodeURIComponent(document.cookie); const decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';'); const ca = decodedCookie.split(";");
name = name + '='; name = `${name}=`;
for (var i = 0; i < ca.length; i++) { for (let i = 0; i < ca.length; i++) {
var c = ca[i]; let c = ca[i];
while (c.charAt(0) == ' ') { while (c.charAt(0) == " ") {
c = c.substring(1); c = c.substring(1);
} }
if (!c.indexOf(name)) { if (!c.indexOf(name)) {
return c.substring(name.length, c.length); return c.substring(name.length, c.length);
}
} }
}
} }
function cookie_set(name, value, days) { function cookie_set(name, value, days) {
var d = new Date(); const d = new Date();
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000); d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
var expires = 'expires=' + d.toUTCString(); const expires = `expires=${d.toUTCString()}`;
document.cookie = name + '=' + value + ';' + expires + ';path=/'; document.cookie = `${name}=${value};${expires};path=/`;
} }
var uuid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+'; const uuid_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+";
function uuid(length) { function uuid(length) {
if (typeof length == 'undefined') { if (typeof length == "undefined") {
length = 52; length = 52;
} }
var s = ''; let s = "";
for (var i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)]; s += uuid_chars[Math.floor(Math.random() * uuid_chars.length)];
} }
return s return s;
} }
$(function () { $(function () {
if (typeof cookie_get('client-id') == 'undefined') { if (typeof cookie_get("client-id") == "undefined") {
cookie_set('client-id', uuid(), 10000); 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';
} }
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) { Vue.filter("number", function (value) {
if (typeof value == 'undefined') { if (isNaN(value)) {
return ''; return "NaN";
} }
if (typeof precision == 'undefined') { return value.toLocaleString();
precision = 2; });
}
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 (typeof precision == "undefined") {
if (!value) { precision = 2;
return ''; }
}
if (typeof precision == 'undefined') { return `${(value * 100.0).toFixed(precision)}%`;
precision = 2; });
}
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 precision == "undefined") {
if (typeof value == 'undefined') { precision = 2;
return '0'; }
}
return parseFloat(value).toFixed(precision) return `${(value * 100.0).toFixed(precision)}%`;
}); });
Vue.filter('upper', function (value) { Vue.filter("fixed", function (value, precision) {
if (typeof value == 'undefined') { if (typeof value == "undefined") {
return ''; return "0";
} }
return value.toUpperCase() return parseFloat(value).toFixed(precision);
}); });
Vue.filter('time', function (value, precision) { Vue.filter("upper", function (value) {
if (isNaN(value)) { if (typeof value == "undefined") {
return ''; return "";
} }
if (isNaN(precision)) { return value.toUpperCase();
precision = 0; });
}
var MIN = 60; Vue.filter("time", function (value, precision) {
var HR = MIN * 60; if (isNaN(value)) {
var DAY = HR * 24; return "";
var parts = []; }
if (DAY <= value) { if (isNaN(precision)) {
parts.push(Math.floor(value / DAY)); precision = 0;
value %= DAY; }
}
if (HR <= value) { const MIN = 60;
parts.push(Math.floor(value / HR)); const HR = MIN * 60;
value %= HR; const DAY = HR * 24;
} const parts = [];
if (MIN <= value) { if (DAY <= value) {
parts.push(Math.floor(value / MIN)); parts.push(Math.floor(value / DAY));
value %= MIN; value %= DAY;
} else { }
parts.push(0);
}
parts.push(value); if (HR <= value) {
parts.push(Math.floor(value / HR));
value %= HR;
}
for (var i = 0; i < parts.length; i++) { if (MIN <= value) {
parts[i] = parts[i].toFixed(i == parts.length - 1 ? precision : 0); parts.push(Math.floor(value / MIN));
if (i && parts[i] < 10) { value %= MIN;
parts[i] = '0' + parts[i]; } else {
} parts.push(0);
} }
return parts.join(':'); parts.push(value);
});
// Vue app for (let i = 0; i < parts.length; i++) {
require('./app'); 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 @@
/******************************************************************************\ "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'
module.exports = { module.exports = {
template: '#message-template', template: "#message-template",
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
required: true, required: true,
twoWay: true twoWay: true
}, },
class: { class: {
type: String, type: String,
required: false, required: false,
twoWay: false twoWay: false
}
} }
} };
}

View File

@@ -1,48 +1,20 @@
/******************************************************************************\ "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'
module.exports = { module.exports = {
replace: true, replace: true,
template: '#modbus-reg-view-template', template: "#modbus-reg-view-template",
props: ['index', 'model', 'template', 'enable'], 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: { methods: {
has_user_value: function () { change: function () {
var type = this.model['reg-type']; this.$dispatch("input-changed");
return type.indexOf('write') != -1 || type.indexOf('fixed') != -1; }
} }
}, };
methods: {
change: function () {this.$dispatch('input-changed')}
}
}

View File

@@ -1,51 +1,23 @@
/******************************************************************************\ "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'
// Must match modbus.c // Must match modbus.c
var exports = { const exports = {
DISCONNECTED: 0, DISCONNECTED: 0,
OK: 1, OK: 1,
CRC: 2, CRC: 2,
INVALID: 3, INVALID: 3,
TIMEDOUT: 4 TIMEDOUT: 4
}; };
exports.status_to_string = exports.status_to_string =
function (status) { function (status) {
if (status == exports.OK) return 'Ok'; switch (status) {
if (status == exports.CRC) return 'CRC error'; case exports.OK: return "Ok";
if (status == exports.INVALID) return 'Invalid response'; case exports.CRC: return "CRC error";
if (status == exports.TIMEDOUT) return 'Timedout'; case exports.INVALID: return "Invalid response";
return 'Disconnected'; case exports.TIMEDOUT: return "Timedout";
} default: return "Disconnected";
}
};
module.exports = exports; module.exports = exports;

View File

@@ -1,120 +1,120 @@
'use strict' "use strict";
module.exports = { module.exports = {
template: '#motor-view-template', template: "#motor-view-template",
props: ['index', 'config', 'template', 'state'], props: ["index", "config", "template", "state"],
computed: { computed: {
metric: function () { metric: function () {
return this.$root.display_units === "METRIC"; return this.$root.display_units === "METRIC";
}, },
is_slave: function () { is_slave: function () {
for (var i = 0; i < this.index; i++) { for (let i = 0; i < this.index; i++) {
if (this.motor.axis == this.config.motors[i].axis) { if (this.motor.axis == this.config.motors[i].axis) {
return true; 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 () { events: {
return this.config.motors[this.index] "input-changed": function () {
}, Vue.nextTick(function () {
// Limit max-velocity
if (this.invalidMaxVelocity) {
this.$set('motor["max-velocity"]', this.maxMaxVelocity);
}
invalidMaxVelocity: function () { //Limit stall-velocity
return this.maxMaxVelocity < this.motor['max-velocity']; if (this.invalidStallVelocity) {
}, this.$set('motor["search-velocity"]', this.maxStallVelocity);
}
maxMaxVelocity: function () { this.$dispatch("config-changed");
return 1 * (15 * this.umPerStep / this.motor['microsteps']).toFixed(3); }.bind(this));
},
ustepPerSec: function () { return false;
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);
} }
},
//Limit stall-velocity methods: {
if (this.invalidStallVelocity) { show: function (name, templ) {
this.$set('motor["search-velocity"]', this.maxStallVelocity); 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 = { module.exports = {
template: "#settings-view-template", template: "#settings-view-template",
attached: function () { attached: function () {
this.svelteComponent = SvelteComponents.createComponent( this.svelteComponent = SvelteComponents.createComponent(
"SettingsView", "SettingsView",
document.getElementById("settings") document.getElementById("settings")
); );
}, },
detached: function() { detached: function() {
this.svelteComponent.$destroy(); 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 if (typeof retry == "undefined") {
All rights reserved. retry = 2000;
}
if (typeof timeout == "undefined") {
timeout = 16000;
}
This file ("the software") is free software: you can redistribute it this.url = url;
and/or modify it under the terms of the GNU General Public License, this.retry = retry;
version 2 as published by the Free Software Foundation. You should this.timeout = timeout;
have received a copy of the GNU General Public License, version 2 this.divisions = 4;
along with the software. If not, see <http://www.gnu.org/licenses/>. this.count = 0;
The software is distributed in the hope that it will be useful, but this.connect();
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 Sock.prototype.onmessage = function () {
License along with the software. If not, see // Ignore
<http://www.gnu.org/licenses/>. };
For information regarding this software email: Sock.prototype.onopen = function () {
"Joseph Coffland" <joseph@buildbotics.com> // Ignore
};
\******************************************************************************/
'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.onclose = function () {
// Ignore
};
Sock.prototype.connect = function () { Sock.prototype.connect = function () {
console.debug('connecting to', this.url); console.debug("connecting to", this.url);
this.close(); this.close();
this._sock = new SockJS(this.url); this._sock = new SockJS(this.url);
this._sock.onmessage = function (e) { this._sock.onmessage = function (e) {
console.debug('msg:', e.data); console.debug("msg:", e.data);
this.heartbeat('msg'); this.heartbeat("msg");
this.onmessage(e); this.onmessage(e);
}.bind(this); }.bind(this);
this._sock.onopen = function () {
console.debug("connected");
this.heartbeat("open");
this.onopen();
}.bind(this);
this._sock.onopen = function () { this._sock.onclose = function () {
console.debug('connected'); console.debug("disconnected");
this.heartbeat('open'); this._cancel_timeout();
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.onclose();
if (typeof this._sock != "undefined") {
setTimeout(this.connect.bind(this), this.retry);
}
}.bind(this);
};
Sock.prototype._timedout = function () { Sock.prototype._timedout = function () {
// Divide timeout so slow browser doesn't trigger timeouts when the // Divide timeout so slow browser doesn't trigger timeouts when the
// connection is good. // connection is good.
if (this.divisions <= ++this.count) { if (this.divisions <= ++this.count) {
console.debug('connection timedout'); console.debug("connection timedout");
this._timeout = undefined; this._timeout = undefined;
this._sock.close(); this._sock.close();
} else this._set_timeout();
}
} else {
this._set_timeout();
}
};
Sock.prototype._cancel_timeout = function () { Sock.prototype._cancel_timeout = function () {
clearTimeout(this._timeout); clearTimeout(this._timeout);
this._timeout = undefined; this._timeout = undefined;
this.count = 0; this.count = 0;
} };
Sock.prototype._set_timeout = function () { Sock.prototype._set_timeout = function () {
this._timeout = setTimeout(this._timedout.bind(this), this._timeout = setTimeout(this._timedout.bind(this),
this.timeout / this.divisions); this.timeout / this.divisions);
} };
Sock.prototype.heartbeat = function (msg) {
//console.debug('heartbeat ' + new Date().toLocaleTimeString() + ' ' + msg);
this._cancel_timeout();
this._set_timeout();
}
Sock.prototype.heartbeat = function () {
this._cancel_timeout();
this._set_timeout();
};
Sock.prototype.close = function () { Sock.prototype.close = function () {
if (typeof this._sock != 'undefined') { if (typeof this._sock != "undefined") {
var sock = this._sock; const sock = this._sock;
this._sock = undefined; this._sock = undefined;
sock.close(); 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 = { module.exports = {
replace: true, replace: true,
template: '#templated-input-template', template: "#templated-input-template",
props: ['name', 'model', 'template'], props: ["name", "model", "template"],
data: function () { data: function () {
return { view: '' } return { view: "" };
},
computed: {
metric: function () {
return this.$root.display_units === "METRIC";
}, },
_view: function () { computed: {
if (this.template.scale) { metric: function () {
if (this.metric) { return this.$root.display_units === "METRIC";
return 1 * this.model.toFixed(3); },
_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 () { watch: {
return (this.metric || !this.template.iunit) _view: function () {
? this.template.unit this.view = this._view;
: this.template.iunit; },
view: function () {
if (this.template.scale && !this.metric) {
this.model = this.view * this.template.scale;
} else {
this.model = this.view;
}
}
}, },
title: function () { ready: function () {
var s = `Default :${this.template.default} ${(this.template.unit || '')}`; this.view = this._view;
if (typeof this.template.help != 'undefined') {
s = this.template.help + '\n' + s;
}
return s;
}
},
watch: {
_view: function () {
this.view = this._view
}, },
view: function () { methods: {
if (this.template.scale && !this.metric) { change: function () {
this.model = this.view * this.template.scale; this.$dispatch("input-changed");
} else { }
this.model = this.view;
}
} }
}, };
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. const api = require("./api");
const modbus = require("./modbus.js");
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 merge = require("lodash.merge"); const merge = require("lodash.merge");
module.exports = { module.exports = {
template: '#tool-view-template', template: "#tool-view-template",
props: ['config', 'template', 'state'], props: ["config", "template", "state"],
data: function () { data: function () {
return { return {
address: 0, address: 0,
value: 0, value: 0,
toolList: [ toolList: [
{ {
id: "disabled", id: "disabled",
name: "Disabled" name: "Disabled"
}, },
{ {
id: "router", id: "router",
type: "PWM Spindle", type: "PWM Spindle",
name: "Router (Makita, etc)" name: "Router (Makita, etc)"
}, },
{ {
id: "laser", id: "laser",
type: "PWM Spindle", type: "PWM Spindle",
name: "Laser (J Tech, etc)" name: "Laser (J Tech, etc)"
}, },
{ {
id: "pwm", id: "pwm",
name: "PWM Spindle" name: "PWM Spindle"
}, },
{ {
id: "unsupported-separator", id: "unsupported-separator",
name: "Unsupported Tools", name: "Unsupported Tools",
disabled: true, disabled: true,
unsupported: true unsupported: true
}, },
{ {
id: "huanyang-vfd", id: "huanyang-vfd",
name: "Huanyang VFD", name: "Huanyang VFD",
unsupported: true unsupported: true
}, },
{ {
id: "custom-modbus-vfd", id: "custom-modbus-vfd",
name: "Custom Modbus VFD", name: "Custom Modbus VFD",
unsupported: true unsupported: true
}, },
{ {
id: "ac-tech-vfd", id: "ac-tech-vfd",
name: "AC-Tech VFD", name: "AC-Tech VFD",
unsupported: true unsupported: true
}, },
{ {
id: "nowforever-vfd", id: "nowforever-vfd",
name: "Nowforever VFD", name: "Nowforever VFD",
unsupported: true unsupported: true
}, },
{ {
id: "delta-vfd", id: "delta-vfd",
name: "Delta VFD015M21A (Beta)", name: "Delta VFD015M21A (Beta)",
unsupported: true unsupported: true
}, },
{ {
id: "yl600-vfd", id: "yl600-vfd",
name: "YL600, YL620, YL620-A VFD (Beta)", name: "YL600, YL620, YL620-A VFD (Beta)",
unsupported: true unsupported: true
}, },
{ {
id: "fr-d700-vfd", id: "fr-d700-vfd",
name: "FR-D700 (Beta)", name: "FR-D700 (Beta)",
unsupported: true unsupported: true
}, },
{ {
id: "sunfar-e300-vfd", id: "sunfar-e300-vfd",
name: "Sunfar E300 (Beta)", name: "Sunfar E300 (Beta)",
unsupported: true unsupported: true
}, },
{ {
id: "omron-mx2-vfd", id: "omron-mx2-vfd",
name: "OMRON MX2", name: "OMRON MX2",
unsupported: true 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 = { module.exports = {
replace: true, replace: true,
template: '{{text}}<span class="unit">{{metric ? unit : iunit}}</span>', template: '{{text}}<span class="unit">{{metric ? unit : iunit}}</span>',
props: ['value', 'precision', 'unit', 'iunit', 'scale'], props: ["value", "precision", "unit", "iunit", "scale"],
computed: { computed: {
metric: { metric: {
cache: false, cache: false,
get: function () { get: function () {
return this.$root.display_units === "METRIC"; 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 () { ready: function () {
var value = this.value; if (typeof this.precision == "undefined") {
if (typeof value == 'undefined') { this.precision = 0;
return ''; }
}
if (!this.metric) { if (typeof this.unit == "undefined") {
value /= this.scale; 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"> <script lang="ts">
import WifiConnectionDialog from "$dialogs/WifiConnectionDialog.svelte"; import WifiConnectionDialog from "$dialogs/WifiConnectionDialog.svelte";
import ChangeHostnameDialog from "$dialogs/ChangeHostnameDialog.svelte"; import ChangeHostnameDialog from "$dialogs/ChangeHostnameDialog.svelte";
import Button, { Label } from "@smui/button"; import Button, { Label } from "@smui/button";
import List, { Item, Graphic, Text, Meta } from "@smui/list"; import List, { Item, Graphic, Text, Meta } from "@smui/list";
import Card from "@smui/card"; import Card from "@smui/card";
import { networkInfo, type WifiNetwork } from "$lib/NetworkInfo"; import { networkInfo, type WifiNetwork } from "$lib/NetworkInfo";
let changeHostnameDialog = { let changeHostnameDialog = {
open: false, open: false,
}; };
let wifiConnectionDialog = { let wifiConnectionDialog = {
open: false, open: false,
network: {} as WifiNetwork, network: {} as WifiNetwork,
}; };
function getWifiStrengthStyle(network: WifiNetwork) { function getWifiStrengthStyle(network: WifiNetwork) {
const strength = Math.ceil((Number(network.Quality) / 100) * 4); const strength = Math.ceil((Number(network.Quality) / 100) * 4);
switch (strength) { switch (strength) {
case 0: case 0:
return "clip-path: circle(0px at 12.5px 19px);"; return "clip-path: circle(0px at 12.5px 19px);";
case 1: case 1:
return "clip-path: circle(4px at 12.5px 19px);"; return "clip-path: circle(4px at 12.5px 19px);";
case 2: case 2:
return "clip-path: circle(8px at 12.5px 19px);"; return "clip-path: circle(8px at 12.5px 19px);";
case 3: case 3:
return "clip-path: circle(14px at 12.5px 19px);"; return "clip-path: circle(14px at 12.5px 19px);";
case 4: case 4:
return ""; return "";
}
} }
}
function onChangeHostname() { function onChangeHostname() {
changeHostnameDialog = { changeHostnameDialog = {
open: true, open: true,
}; };
} }
function onNetworkSelected(network: WifiNetwork) { function onNetworkSelected(network: WifiNetwork) {
wifiConnectionDialog = { wifiConnectionDialog = {
open: true, open: true,
network, network,
}; };
} }
</script> </script>
<WifiConnectionDialog {...wifiConnectionDialog} /> <WifiConnectionDialog {...wifiConnectionDialog} />
<ChangeHostnameDialog {...changeHostnameDialog} /> <ChangeHostnameDialog {...changeHostnameDialog} />
<div class="admin-network-view"> <div class="admin-network-view">
<h1>Network Info</h1> <h1>Network Info</h1>
<div class="pure-form pure-form-aligned"> <div class="pure-form pure-form-aligned">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="hostname">Hostname</label> <label for="hostname">Hostname</label>
<Card id="hostname" variant="outlined"> <Card id="hostname" variant="outlined">
<Text id="hostname"> <Text id="hostname">
{$networkInfo.hostname} {$networkInfo.hostname}
</Text> </Text>
</Card> </Card>
<Button on:click={onChangeHostname} touch variant="raised"> <Button on:click={onChangeHostname} touch variant="raised">
<Label>Change</Label> <Label>Change</Label>
</Button> </Button>
</div>
</div> </div>
</div>
<div class="pure-form pure-form-aligned"> <div class="pure-form pure-form-aligned">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="ip-addresses">IP Addresses</label> <label for="ip-addresses">IP Addresses</label>
<Card id="ip-addresses" variant="outlined"> <Card id="ip-addresses" variant="outlined">
{#each $networkInfo.ipAddresses as ipAddress} {#each $networkInfo.ipAddresses as ipAddress}
<div> <div>
<Text id="hostname"> <Text id="hostname">
{ipAddress} {ipAddress}
</Text> </Text>
</div> </div>
{/each} {/each}
</Card> </Card>
</div>
</div> </div>
</div>
<div class="pure-form pure-form-aligned"> <div class="pure-form pure-form-aligned">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="wifi">Wi-Fi</label> <label for="wifi">Wi-Fi</label>
<div class="wifi-networks"> <div class="wifi-networks">
<Card id="wifi" variant="outlined"> <Card id="wifi" variant="outlined">
<List> <List>
{#if $networkInfo.wifi.networks.length === 0} {#if $networkInfo.wifi.networks.length === 0}
<Item class="wifi-network"> <Item class="wifi-network">
<Text>Scanning...</Text> <Text>Scanning...</Text>
</Item> </Item>
{:else} {:else}
{#each $networkInfo.wifi.networks as network} {#each $networkInfo.wifi.networks as network}
<Item <Item
class="wifi-network" class="wifi-network"
on:SMUI:action={() => onNetworkSelected(network)} on:SMUI:action={() =>
> onNetworkSelected(network)}
<Graphic >
class="strength {$networkInfo.wifi.ssid === network.Name <Graphic
? 'active' class="strength {$networkInfo.wifi
: ''}" .ssid === network.Name
> ? 'active'
<span class="fa fa-wifi background" /> : ''}"
<span >
class="fa fa-wifi" <span class="fa fa-wifi background" />
style={getWifiStrengthStyle(network)} <span
/> class="fa fa-wifi"
</Graphic> style={getWifiStrengthStyle(
<Text style="margin-right: 20px;">{network.Name}</Text> network
{#if network.Encryption !== "Open"} )}
<Meta> />
<span class="fa fa-lock" /> </Graphic>
</Meta> <Text style="margin-right: 20px;"
{/if} >{network.Name}</Text
</Item> >
{/each} {#if network.Encryption !== "Open"}
{/if} <Meta>
</List> <span class="fa fa-lock" />
</Card> </Meta>
<em style="display: block;"> {/if}
Click on a Wi-Fi network to connect or disconnect. </Item>
</em> {/each}
</div> {/if}
</List>
</Card>
<em style="display: block;">
Click on a Wi-Fi network to connect or disconnect.
</em>
</div>
</div>
</div> </div>
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
$primary: #0078e7; $primary: #0078e7;
$very-dark: #555; $very-dark: #555;
$text: #777; $text: #777;
$grey: #bbb; $grey: #bbb;
$light: #ddd; $light: #ddd;
:global { :global {
.admin-network-view { .admin-network-view {
.pure-form-aligned .pure-control-group label { .pure-form-aligned .pure-control-group label {
vertical-align: top; vertical-align: top;
font-size: 15pt; font-size: 15pt;
font-weight: bold; font-weight: bold;
} }
button { button {
margin: 0; margin: 0;
} }
.mdc-card { .mdc-card {
width: 400px; width: 400px;
min-height: 38px; min-height: 38px;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-bottom: 20px; margin-bottom: 20px;
margin-right: 20px; margin-right: 20px;
padding: 5px 15px; padding: 5px 15px;
} }
.wifi-networks { .wifi-networks {
display: inline-block; display: inline-block;
.mdc-card { .mdc-card {
padding: 0; padding: 0;
margin-bottom: 5px; margin-bottom: 5px;
} }
} }
.wifi-network { .wifi-network {
.lock { .lock {
font-size: 20px; font-size: 20px;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
.strength { .strength {
border-radius: 50%; border-radius: 50%;
padding: 3px; padding: 3px;
background-color: $light; background-color: $light;
color: $very-dark; color: $very-dark;
margin-right: 10px; margin-right: 10px;
position: relative; position: relative;
&.active { &.active {
background-color: $primary; background-color: $primary;
color: white; color: white;
} }
span { span {
position: absolute; position: absolute;
top: 5px; top: 5px;
font-size: 22px; font-size: 22px;
&.background { &.background {
opacity: 0.25; opacity: 0.25;
}
}
}
} }
}
} }
}
} }
}
</style> </style>

View File

@@ -1,131 +1,131 @@
<script lang="ts"> <script lang="ts">
import configTemplate from "../../../resources/config-template.json"; import configTemplate from "../../../resources/config-template.json";
import { Config, DisplayUnits } from "$lib/ConfigStore"; import { Config, DisplayUnits } from "$lib/ConfigStore";
import { ControllerMethods } from "$lib/RegisterControllerMethods"; import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { onMount } from "svelte"; import { onMount } from "svelte";
type Template = { type Template = {
type?: string; type?: string;
values?: (string | number)[]; values?: (string | number)[];
unit?: "string"; unit?: "string";
iunit?: "string"; iunit?: "string";
min?: number; min?: number;
max?: number; max?: number;
step?: number; step?: number;
help?: string; help?: string;
default?: string | number; default?: string | number;
scale?: number; scale?: number;
}; };
export let key: string; export let key: string;
let keyParts: string[]; let keyParts: string[];
let template: Template; let template: Template;
let name: string; let name: string;
let title: string; let title: string;
let units: string; let units: string;
let value; let value;
onMount(() => { onMount(() => {
keyParts = (key || "").split("."); keyParts = (key || "").split(".");
template = getTemplate(); template = getTemplate();
name = keyParts[keyParts.length - 1]; name = keyParts[keyParts.length - 1];
title = getTitle(); title = getTitle();
value = getValue(); 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;
}); });
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> </script>
{#if template} {#if template}
<div class="pure-control-group" {title}> <div class="pure-control-group" {title}>
<label for={name}>{name}</label> <label for={name}>{name}</label>
{#if template.values} {#if template.values}
<select {name} bind:value on:change={onChange}> <select {name} bind:value on:change={onChange}>
{#each template.values as opt} {#each template.values as opt}
<option value={opt} disabled={opt === "-----"}> <option value={opt} disabled={opt === "-----"}>
{opt} {opt}
</option> </option>
{/each} {/each}
</select> </select>
{:else if template.type === "bool"} {:else if template.type === "bool"}
<input {name} type="checkbox" bind:value on:input={onChange} /> <input {name} type="checkbox" bind:value on:input={onChange} />
{:else if template.type === "float"} {:else if template.type === "float"}
<input <input
{name} {name}
type="number" type="number"
min={template.min} min={template.min}
max={template.max} max={template.max}
step={template.step || "any"} step={template.step || "any"}
bind:value bind:value
on:input={onChange} on:input={onChange}
/> />
{:else if template.type === "int"} {:else if template.type === "int"}
<input <input
{name} {name}
type="number" type="number"
min={template.min} min={template.min}
max={template.max} max={template.max}
bind:value bind:value
on:input={onChange} on:input={onChange}
/> />
{:else if template.type === "string"} {:else if template.type === "string"}
<input {name} type="text" bind:value on:input={onChange} /> <input {name} type="text" bind:value on:input={onChange} />
{:else if template.type == "text"} {:else if template.type == "text"}
<textarea {name} bind:value on:input={onChange} /> <textarea {name} bind:value on:input={onChange} />
{/if} {/if}
<label for="" class="units">{units || ""}</label> <label for="" class="units">{units || ""}</label>
<slot name="extra" /> <slot name="extra" />
</div> </div>
{/if} {/if}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,444 +1,455 @@
<script type="ts"> <script type="ts">
import Dialog, { Title, Content, Actions } from "@smui/dialog"; import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button"; import Button, { Label } from "@smui/button";
import { waitForChange } from "$lib/StoreHelpers"; import { waitForChange } from "$lib/StoreHelpers";
import { ControllerMethods } from "$lib/RegisterControllerMethods"; import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { Config } from "$lib/ConfigStore"; import { Config } from "$lib/ConfigStore";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import { import {
probingActive, probingActive,
probeContacted, probeContacted,
probingComplete, probingComplete,
probingFailed, probingFailed,
probingStarted, probingStarted,
} from "$lib/ControllerState"; } from "$lib/ControllerState";
import { numberWithUnit } from "$lib/RegexHelpers"; import { numberWithUnit } from "$lib/RegexHelpers";
import TextFieldWithOptions from "$components/TextFieldWithOptions.svelte"; import TextFieldWithOptions from "$components/TextFieldWithOptions.svelte";
const ValidSteps = [ const ValidSteps = [
"None", "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 = [
"CheckProbe", "CheckProbe",
probeType === "xyz" ? "BitDimensions" : undefined, "BitDimensions",
"PlaceProbeBlock", "PlaceProbeBlock",
"Probe", "Probe",
"Done", "Done",
].filter<Step>(isStep); ] as const;
await stepCompleted("CheckProbe", probeContacted); type Step = typeof ValidSteps[number];
if (probeType === "xyz") { function isStep(str): str is Step {
await stepCompleted("BitDimensions", userAcknowledged); return ValidSteps.includes(str);
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([ const stepLabels: Record<Step, string> = {
...writables.map((writable) => waitForChange(writable)), None: "",
waitForChange(cancelled), CheckProbe: "Check probe",
]); BitDimensions: "Bit dimensions",
PlaceProbeBlock: "Place probe block",
if ($cancelled) { Probe: "Probe",
throw new Error("cancelled"); Done: "Done",
}
}
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,
}; };
switch (currentStep) { const cancelled = writable(false);
case "CheckProbe": const userAcknowledged = writable(false);
case "Probe":
nextButton.disabled = true;
break;
case "BitDimensions": const imperialBits: `${number}/${number} in`[] = [
nextButton.disabled = !isFinite(cutterDiameterMetric); "1/2 in",
break; "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 = { nextButton = {
disabled: false, label: "Next",
label: "Done", disabled: false,
allowClose: true, 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() { function executeProbe() {
const probeBlockWidth = $Config.probe["probe-xdim"]; const probeBlockWidth = $Config.probe["probe-xdim"];
const probeBlockLength = $Config.probe["probe-ydim"]; const probeBlockLength = $Config.probe["probe-ydim"];
const probeBlockHeight = $Config.probe["probe-zdim"]; const probeBlockHeight = $Config.probe["probe-zdim"];
const slowSeek = $Config.probe["probe-slow-seek"]; const slowSeek = $Config.probe["probe-slow-seek"];
const fastSeek = $Config.probe["probe-fast-seek"]; const fastSeek = $Config.probe["probe-fast-seek"];
const cutterLength = 12.7; const cutterLength = 12.7;
const zLift = 1; const zLift = 1;
const xOffset = probeBlockWidth + cutterDiameterMetric / 2.0; const xOffset = probeBlockWidth + cutterDiameterMetric / 2.0;
const yOffset = probeBlockLength + cutterDiameterMetric / 2.0; const yOffset = probeBlockLength + cutterDiameterMetric / 2.0;
const zOffset = probeBlockHeight; const zOffset = probeBlockHeight;
if (probeType === "z") { if (probeType === "z") {
ControllerMethods.send(` ControllerMethods.send(`
G21 G21
G92 Z0 G92 Z0
G38.2 Z -25.4 F${fastSeek} G38.2 Z -25.4 F${fastSeek}
G91 G1 Z 1 G91 G1 Z 1
G38.2 Z -2 F${slowSeek} G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset} G92 Z ${zOffset}
G91 G0 Z 3 G91 G0 Z 3
M2 M2
`); `);
} else { } else {
// After probing Z, we want to drop the bit down: // After probing Z, we want to drop the bit down:
// Ideally, 12.7mm/0.5in // Ideally, 12.7mm/0.5in
// And we don't want to be more than 90% down on the probe block // 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 // Also, add zlift to compensate for the fact that we lift after probing Z
const plunge = Math.min(cutterLength, zOffset * 0.9) + zLift; const plunge = Math.min(cutterLength, zOffset * 0.9) + zLift;
ControllerMethods.send(` ControllerMethods.send(`
G21 G21
G92 X0 Y0 Z0 G92 X0 Y0 Z0
G38.2 Z -25 F${fastSeek} G38.2 Z -25 F${fastSeek}
G91 G1 Z 1 G91 G1 Z 1
G38.2 Z -2 F${slowSeek} G38.2 Z -2 F${slowSeek}
G92 Z ${zOffset} G92 Z ${zOffset}
G91 G0 Z ${zLift} G91 G0 Z ${zLift}
G91 G0 X 20 G91 G0 X 20
G91 G0 Z ${-plunge} G91 G0 Z ${-plunge}
G38.2 X -20 F${fastSeek} G38.2 X -20 F${fastSeek}
G91 G1 X 1 G91 G1 X 1
G38.2 X -2 F${slowSeek} G38.2 X -2 F${slowSeek}
G92 X ${xOffset} G92 X ${xOffset}
G91 G0 X 1 G91 G0 X 1
G91 G0 Y 20 G91 G0 Y 20
G91 G0 X -20 G91 G0 X -20
G38.2 Y -20 F${fastSeek} G38.2 Y -20 F${fastSeek}
G91 G1 Y 1 G91 G1 Y 1
G38.2 Y -2 F${slowSeek} G38.2 Y -2 F${slowSeek}
G92 Y ${yOffset} G92 Y ${yOffset}
G91 G0 Y 3 G91 G0 Y 3
G91 G0 Z 25 G91 G0 Z 25
M2 M2
`); `);
}
} }
}
</script> </script>
<Dialog <Dialog
bind:open bind:open
class="probe-dialog" class="probe-dialog"
scrimClickAction="" scrimClickAction=""
aria-labelledby="probe-dialog-title" aria-labelledby="probe-dialog-title"
aria-describedby="probe-dialog-content" aria-describedby="probe-dialog-content"
surface$style="width: 700px; max-width: calc(100vw - 32px);" 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;"> <Content id="probe-dialog-content" style="overflow: visible;">
<div class="steps"> <div class="steps">
<p><b>Step {steps.indexOf(currentStep) + 1} of {steps.length}</b></p> <p>
<ul> <b>Step {steps.indexOf(currentStep) + 1} of {steps.length}</b>
{#each steps as step} </p>
<li class:active={currentStep === step}>{stepLabels[step]}</li> <ul>
{/each} {#each steps as step}
</ul> <li class:active={currentStep === step}>
</div> {stepLabels[step]}
<p> </li>
{#if currentStep === "CheckProbe"} {/each}
Attach the probe magnet to the collet, then touch the probe block to the </ul>
bit. </div>
{:else if currentStep === "BitDimensions"} <p>
<TextFieldWithOptions {#if currentStep === "CheckProbe"}
label="Cutter diameter" Attach the probe magnet to the collet, then touch the probe
variant="filled" block to the bit.
spellcheck="false" {:else if currentStep === "BitDimensions"}
style="width: 100%;" <TextFieldWithOptions
bind:value={cutterDiameterString} label="Cutter diameter"
options={[imperialBits, metricBits]} variant="filled"
valid={isFinite(cutterDiameterMetric)} spellcheck="false"
helperText={`Examples: 1/2", 10 mm, 0.25 in`} style="width: 100%;"
/> bind:value={cutterDiameterString}
{:else if currentStep === "PlaceProbeBlock"} options={[imperialBits, metricBits]}
{#if probeType === "xyz"} valid={isFinite(cutterDiameterMetric)}
Place the probe block face up, on the lower-left corner of your helperText={`Examples: 1/2", 10 mm, 0.25 in`}
workpiece. />
{:else} {:else if currentStep === "PlaceProbeBlock"}
Place the probe block face down, with the bit above the recess. {#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} {/if}
<Button
<p>The probing procedure will begin as soon as you click 'Next'.</p> defaultAction
{:else if currentStep === "Probe"} data-mdc-dialog-action={nextButton.allowClose ? "close" : ""}
Probing in progress... disabled={nextButton.disabled}
{:else if currentStep === "Done"} on:click={() => ($userAcknowledged = true)}
{#if $probingFailed} >
Could not find the probe block during probing! <Label>
{nextButton.label}
<p> </Label>
Make sure the tip of the bit is less than {metric ? "25mm" : "1 in"} </Button>
above the probe block, and try again. </Actions>
</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>
</Dialog> </Dialog>
<style lang="scss"> <style lang="scss">
$primary: #0078e7; $primary: #0078e7;
$very-dark: #555; $very-dark: #555;
$text: #777; $text: #777;
$grey: #bbb; $grey: #bbb;
$light: #ddd; $light: #ddd;
:global { :global {
#probe-dialog-content { #probe-dialog-content {
display: flex; display: flex;
flex-direction: row; 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> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,15 +31,15 @@ const empty: NetworkInfo = {
ssid: "", ssid: "",
networks: [] networks: []
} }
} };
export const networkInfo = writable<NetworkInfo>(empty); export const networkInfo = writable<NetworkInfo>(empty);
export function processNetworkInfo(rawNetworkInfo: NetworkInfo) { export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
const now = Date.now(); 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) { if (network.Name) {
network.lastSeen = now; network.lastSeen = now;
network.active = rawNetworkInfo.wifi.ssid === network.Name; 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) { if (network.lastSeen - now > 30000) {
delete networksByName[network.Name]; delete networksByName[network.Name];
} }

View File

@@ -1,9 +1,3 @@
type NumberWithUnit = {
value: number,
metric: boolean,
toMetric: () => number;
}
function numberWithUnitToMetric() { function numberWithUnitToMetric() {
return this.metric ? this.value : this.value * 25.4; return this.metric ? this.value : this.value * 25.4;
} }
@@ -38,12 +32,13 @@ function formatFraction(value: number) {
export const numberWithUnit = { export const numberWithUnit = {
regex: /^\s*(?:(\d+)\s*\/\s*(\d+)|(\d*\.\d+)|(\d+(?:\.\d+)?))\s*("|in|inch|inches|mm|millimeters)\s*$/, regex: /^\s*(?:(\d+)\s*\/\s*(\d+)|(\d*\.\d+)|(\d+(?:\.\d+)?))\s*("|in|inch|inches|mm|millimeters)\s*$/,
parse: function (str: string) { parse: function (str: string) {
// eslint-disable-next-line prefer-const
let [, numerator, denominator, decimal1, decimal2, unit]: any = str?.match(numberWithUnit.regex) ?? []; let [, numerator, denominator, decimal1, decimal2, unit]: any = str?.match(numberWithUnit.regex) ?? [];
numerator = Number.parseFloat(numerator) numerator = Number.parseFloat(numerator);
denominator = Number.parseFloat(denominator) denominator = Number.parseFloat(denominator);
decimal1 = Number.parseFloat(decimal1) decimal1 = Number.parseFloat(decimal1);
decimal2 = Number.parseFloat(decimal2) decimal2 = Number.parseFloat(decimal2);
const metric = (unit ?? "").includes("m"); const metric = (unit ?? "").includes("m");
@@ -81,10 +76,10 @@ export const numberWithUnit = {
return ""; return "";
case value.metric: case value.metric:
return `${value.value} mm` return `${value.value} mm`;
default: 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, ...config,
method, method,
cache: "no-cache", cache: "no-cache",
body: (typeof data === 'object') body: (typeof data === "object")
? JSON.stringify(data) ? JSON.stringify(data)
: undefined, : 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(); return await response.json();
} catch (error) { } catch (error) {
console.debug('API Error: ' + url + ': ' + error); console.debug(`API Error: ${url}: ${error}`);
throw error; throw error;
} }
} }
export async function GET(url: string, config: RequestInit = {}) { 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 = {}) { 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 = {}) { 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 = {}) { 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"; import matchAll from "string.prototype.matchall";
matchAll.shim(); matchAll.shim();
import AdminNetworkView from '$components/AdminNetworkView.svelte'; import AdminNetworkView from "$components/AdminNetworkView.svelte";
import SettingsView from '$components/SettingsView.svelte'; import SettingsView from "$components/SettingsView.svelte";
import DialogHost, { showDialog } from "$dialogs/DialogHost.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 { handleControllerStateUpdate } from "$lib/ControllerState";
import { registerControllerMethods } from "$lib/RegisterControllerMethods"; import { registerControllerMethods } from "$lib/RegisterControllerMethods";
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) { export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
switch (component) { switch (component) {
case "AdminNetworkView": case "AdminNetworkView":
return new AdminNetworkView({ target, props }); return new AdminNetworkView({ target, props });
case "SettingsView": case "SettingsView":
return new SettingsView({ target, props }); return new SettingsView({ target, props });
case "DialogHost": case "DialogHost":
return new DialogHost({ target, props }); return new DialogHost({ target, props });
default: default:
throw new Error("Unknown component"); throw new Error("Unknown component");
} }
} }
export { export {
showDialog, showDialog,
handleControllerStateUpdate, handleControllerStateUpdate,
handleConfigUpdate, handleConfigUpdate,
registerControllerMethods, registerControllerMethods,
setDisplayUnits setDisplayUnits
}; };