feat: merge upstream master - version 4.94

Merge upstream PiKVM master branch updates:

- Bump version from 4.93 to 4.94
- HID: improved jiggler pattern for better compatibility
- Streamer: major refactoring for improved performance and maintainability
- Prometheus: tidying GPIO channel name formatting
- Web: added __gpio-label class for custom styling
- HID: customizable /api/hid/print delay configuration
- ATX: independent power/reset regions for better control
- OLED: added --fill option for display testing
- Web: improved keyboard handling in modal dialogs
- Web: enhanced login error messages
- Switch: added heartbeat functionality
- Web: mouse touch code simplification and refactoring
- Configs: use systemd-networkd-wait-online --any by default
- PKGBUILD: use cp -r to install systemd units properly
- Various bug fixes and performance improvements
This commit is contained in:
mofeng-git
2025-08-21 11:21:41 +08:00
205 changed files with 9359 additions and 4653 deletions

View File

@@ -23,6 +23,9 @@
"use strict";
import {ROOT_PREFIX} from "./vars.js";
export var browser = new function() {
// https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser/9851769
// https://github.com/fingerprintjs/fingerprintjs/discussions/641
@@ -133,12 +136,12 @@ export function checkBrowser(desktop_css, mobile_css) {
let force_desktop = (new URL(window.location.href)).searchParams.get("force_desktop");
let force_mobile = (new URL(window.location.href)).searchParams.get("force_mobile");
if ((force_desktop || !browser.is_mobile) && !force_mobile) {
__addCssLink("/share/css/x-desktop.css");
__addCssLink("x-desktop.css");
if (desktop_css) {
__addCssLink(desktop_css);
}
} else {
__addCssLink("/share/css/x-mobile.css");
__addCssLink("x-mobile.css");
if (mobile_css) {
__addCssLink(mobile_css);
}
@@ -148,6 +151,7 @@ export function checkBrowser(desktop_css, mobile_css) {
}
function __addCssLink(path) {
path = `${ROOT_PREFIX}share/css/${path}`;
console.log("===== Adding CSS:", path);
let el_head = document.getElementsByTagName("head")[0];
let el_link = document.createElement("link");

View File

@@ -24,6 +24,7 @@
"use strict";
import {ROOT_PREFIX} from "../vars.js";
import {tools, $} from "../tools.js";
import {checkBrowser} from "../bb.js";
import {wm, initWindowManager} from "../wm.js";
@@ -39,68 +40,86 @@ export function main() {
function __loadKvmdInfo() {
tools.httpGet("/api/info", {"fields": "auth,meta,extras"}, function(http) {
if (http.status === 200) {
let info = JSON.parse(http.responseText).result;
tools.httpGet("api/info", {"fields": "auth,meta,extras"}, function(http) {
switch (http.status) {
case 200:
__showKvmdInfo(JSON.parse(http.responseText).result);
break;
let apps = [];
if (info.extras === null) {
wm.error("Not all applications in the menu can be displayed due an error.<br>See KVMD logs for details.");
} else {
apps = Object.values(info.extras).sort(function(a, b) {
if (a.place < b.place) {
return -1;
} else if (a.place > b.place) {
return 1;
} else {
return 0;
}
});
}
case 401:
case 403:
tools.currentOpen("login");
break;
$("apps-box").innerHTML = "<ul id=\"apps\"></ul>";
// Don't use this option, it may be removed in any time
let hide_kvm_button = (
(info.meta !== null && info.meta.web && info.meta.web.hide_kvm_button)
|| tools.config.getBool("index--hide-kvm-button", false)
);
if (!hide_kvm_button) {
$("apps").innerHTML += __makeApp(null, "kvm", "share/svg/kvm.svg", "KVM");
}
for (let app of apps) {
if (app.place >= 0 && (app.enabled || app.started)) {
$("apps").innerHTML += __makeApp(null, app.path, app.icon, app.name);
}
}
if (info.auth.enabled) {
$("apps").innerHTML += __makeApp("logout-button", "#", "share/svg/logout.svg", "Logout");
tools.el.setOnClick($("logout-button"), __logout);
}
if (info.meta !== null && info.meta.server && info.meta.server.host) {
$("kvmd-meta-server-host").innerHTML = info.meta.server.host;
document.title = `One-KVM Index: ${info.meta.server.host}`;
} else {
$("kvmd-meta-server-host").innerHTML = "";
document.title = "One-KVM Index";
}
} else if (http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
setTimeout(__loadKvmdInfo, 1000);
default:
setTimeout(__loadKvmdInfo, 1000);
break;
}
});
}
function __showKvmdInfo(info) {
let apps = [];
if (info.extras === null) {
wm.error("Not all applications in the menu can be displayed due an error.<br>See KVMD logs for details.");
} else {
apps = Object.values(info.extras).sort(function(a, b) {
if (a.place < b.place) {
return -1;
} else if (a.place > b.place) {
return 1;
} else {
return 0;
}
});
}
let html = "";
// Don't use this option, it may be removed in any time
let hide_kvm_button = (
(info.meta !== null && info.meta.web && info.meta.web.hide_kvm_button)
|| tools.config.getBool("index--hide-kvm-button", false)
);
if (!hide_kvm_button) {
html += __makeApp(null, "kvm", "share/svg/kvm.svg", "KVM");
}
for (let app of apps) {
if (app.place >= 0 && (app.enabled || app.started)) {
html += __makeApp(null, app.path, app.icon, app.name);
}
}
if (info.auth.enabled) {
html += __makeApp("logout-button", "#", "share/svg/logout.svg", "Logout");
}
$("apps-box").innerHTML = `<ul id="apps">${html}</ul>`;
if (info.auth.enabled) {
tools.el.setOnClick($("logout-button"), __logout);
}
if (info.meta !== null && info.meta.server && info.meta.server.host) {
$("kvmd-meta-server-host").innerText = info.meta.server.host;
document.title = `${info.meta.server.host} | PiKVM Index`;
} else {
$("kvmd-meta-server-host").innerHTML = "<i>Invalid meta</i>";
document.title = "PiKVM Index";
}
}
function __makeApp(id, path, icon, name) {
// Tailing slash in href is added to avoid Nginx 301 redirect
// when the location doesn't have tailing slash: "foo -> foo/".
// Reverse proxy over PiKVM can be misconfigured to handle this.
let e_add_id = (id ? `id="${tools.escape(id)}"` : "");
return `<li>
<div ${id ? "id=\"" + id + "\"" : ""} class="app">
<a href="${path}">
<div ${e_add_id} class="app">
<a href="${tools.escape(ROOT_PREFIX + path)}/">
<div>
<img class="svg-gray" src="${icon}">
<img class="svg-gray" src="${tools.escape(ROOT_PREFIX + icon)}">
${tools.escape(name)}
</div>
</a>
@@ -109,11 +128,17 @@ function __makeApp(id, path, icon, name) {
}
function __logout() {
tools.httpPost("/api/auth/logout", null, function(http) {
if (http.status === 200 || http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
wm.error("Logout error", http.responseText);
tools.httpPost("api/auth/logout", null, function(http) {
switch (http.status) {
case 200:
case 401:
case 403:
tools.currentOpen("login");
break;
default:
wm.error("Logout error", http.responseText);
break;
}
});
}

View File

@@ -31,30 +31,44 @@ export function main() {
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", null, function(http) {
if (http.status === 200) {
let ipmi_port = JSON.parse(http.responseText).result.extras.ipmi.port;
let make_item = (comment, ipmi, api) => `
<span class="code-comment"># ${comment}:<br>$</span>
ipmitool -I lanplus -U admin -P admin -H ${window.location.hostname} -p ${ipmi_port} ${ipmi}<br>
<span class="code-comment">$</span> curl -XPOST -HX-KVMD-User:admin -HX-KVMD-Passwd:admin -k \\<br>
&nbsp;&nbsp;&nbsp;&nbsp;${window.location.protocol}//${window.location.host}/api/atx${api}<br>
`;
$("ipmi-text").innerHTML = `
${make_item("Power on the server if it's off", "power on", "/power?action=on")}
<br>
${make_item("Soft power off the server if it's on", "power soft", "/power?action=off")}
<br>
${make_item("Hard power off the server if it's on", "power off", "/power?action=off_hard")}
<br>
${make_item("Hard reset the server if it's on", "power reset", "/power?action=reset_hard")}
<br>
${make_item("Check the power status", "power status", "")}
`;
} else if (http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
setTimeout(__loadKvmdInfo, 1000);
tools.httpGet("api/info", null, function(http) {
switch (http.status) {
case 200:
__showKvmdInfo(JSON.parse(http.responseText).result);
break;
case 401:
case 403:
tools.currentOpen("login");
break;
default:
setTimeout(__loadKvmdInfo, 1000);
break;
}
});
}
function __showKvmdInfo(info) {
let make_item = function (comment, cmd, api) {
return `
<span class="code-comment">
# ${tools.escape(comment)}:<br>$
</span>
ipmitool -I lanplus -U admin -P admin
-H ${tools.escape(window.location.hostname)}
-p ${tools.escape(info.extras.ipmi.port)} ${tools.escape(cmd)}
<br>
<span class="code-comment">$</span>
curl -XPOST -HX-KVMD-User:admin -HX-KVMD-Passwd:admin -k \\<br>&nbsp;&nbsp;&nbsp;&nbsp;
${tools.escape(window.location.protocol + "//" + window.location.host + "/api/atx" + api)}
`;
};
$("ipmi-text").innerHTML = [
make_item("Power on the server if it's off", "power on", "/power?action=on"),
make_item("Soft power off the server if it's on", "power soft", "/power?action=off"),
make_item("Hard power off the server if it's on", "power off", "/power?action=off_hard"),
make_item("Hard reset the server if it's on", "power reset", "/power?action=reset_hard"),
make_item("Check the power status", "power status", ""),
].join("<br><br>");
}

View File

@@ -23,88 +23,82 @@
"use strict";
import {tools, $$$} from "./tools.js";
import {tools} from "./tools.js";
export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
export function Keypad(__el_keypad, __sendKey, __apply_fixes) {
var self = this;
/************************************************************************/
var __merged = {};
var __keys = {};
var __modifiers = {};
var __hold_timers = {};
var __fix_mac_cmd = false;
var __fix_win_altgr = false;
var __altgr_ctrl_timer = null;
var __init__ = function() {
__el_keypad.addEventListener("contextmenu", (ev) => ev.preventDefault());
if (__apply_fixes) {
__fix_mac_cmd = tools.browser.is_mac;
if (__fix_mac_cmd) {
tools.info(`Keymap at ${__keys_parent}: enabled Fix-Mac-CMD`);
tools.info(`Keymap at ${__el_keypad.id}: enabled Fix-Mac-CMD`);
}
__fix_win_altgr = tools.browser.is_win;
if (__fix_win_altgr) {
tools.info(`Keymap at ${__keys_parent}: enabled Fix-Win-AltGr`);
tools.info(`Keymap at ${__el_keypad.id}: enabled Fix-Win-AltGr`);
}
}
for (let el_key of $$$(`${__keys_parent} div.key`)) {
for (let el_key of [].slice.call(__el_keypad.getElementsByClassName("key"))) {
if (el_key.hasAttribute("data-allow-autohold")) {
el_key.title = "Long left click or short right click for hold, middle for lock";
} else {
el_key.title = "Right click for hold, middle for lock";
}
let code = el_key.getAttribute("data-code");
tools.setDefault(__keys, code, []);
__keys[code].push(el_key);
tools.setDefault(__merged, code, []);
__merged[code].push(el_key);
tools.el.setOnDown(el_key, () => __clickHandler(el_key, true));
tools.el.setOnUp(el_key, () => __clickHandler(el_key, false));
tools.el.setOnDown(el_key, (ev) => __clickHandler(el_key, ev));
tools.el.setOnUp(el_key, () => __clickHandler(el_key, null));
el_key.onmouseout = function() {
if (__isPressed(el_key)) {
__clickHandler(el_key, false);
if (
__isActive(el_key, "pressed")
&& !__isActive(el_key, "holded")
&& !__isActive(el_key, "locked")
) {
__clickHandler(el_key, null);
}
};
}
for (let el_key of $$$(`${__keys_parent} div.modifier`)) {
let code = el_key.getAttribute("data-code");
tools.setDefault(__modifiers, code, []);
__modifiers[code].push(el_key);
tools.setDefault(__merged, code, []);
__merged[code].push(el_key);
tools.el.setOnDown(el_key, () => __toggleModifierHandler(el_key));
}
};
/************************************************************************/
self.releaseAll = function() {
for (let dict of [__keys, __modifiers]) {
for (let code in dict) {
if (__isActive(dict[code][0])) {
self.emitByCode(code, false);
}
for (let code in __keys) {
if (__isActive(__keys[code][0])) {
self.emitByCode(code, false);
}
}
};
self.emitByKeyEvent = function(event, state) {
if (event.repeat) {
self.emitByKeyEvent = function(ev, state) {
if (ev.repeat) {
return;
}
let code = event.code;
let code = ev.code;
if (__apply_fixes) {
// https://github.com/pikvm/pikvm/issues/819
if (code == "IntlBackslash" && ["`", "~"].includes(event.key)) {
if (code === "IntlBackslash" && ["`", "~"].includes(ev.key)) {
code = "Backquote";
} else if (code == "Backquote" && ["§", "±"].includes(event.key)) {
} else if (code === "Backquote" && ["§", "±"].includes(ev.key)) {
code = "IntlBackslash";
}
}
@@ -113,7 +107,10 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
};
self.emitByCode = function(code, state, apply_fixes=true) {
if (code in __merged) {
if (code in __keys) {
let el_key = __keys[code][0];
__stopHoldTimer(el_key);
if (__fix_win_altgr && apply_fixes) {
if (!__fixWinAltgr(code, state)) {
return;
@@ -122,13 +119,21 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
if (__fix_mac_cmd && apply_fixes) {
__fixMacCmd(code, state);
}
__commonHandler(__merged[code][0], state, false);
__unholdModifiers();
}
if (state && !__isActive(el_key)) {
__deactivate(el_key);
__activate(el_key, "pressed");
__process(el_key, true);
} else {
__deactivate(el_key);
__process(el_key, false);
}
__unholdAll();
};
};
var __fixMacCmd = function(code, state) {
if ((code == "MetaLeft" || code == "MetaRight") && !state) {
if ((code === "MetaLeft" || code === "MetaRight") && !state) {
for (code in __keys) {
if (__isActive(__keys[code][0])) {
self.emitByCode(code, false, false);
@@ -148,7 +153,7 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
self.emitByCode("ControlLeft", true, false);
}
}
if (code === "ControlLeft" && !__isActive(__modifiers["ControlLeft"][0])) {
if (code === "ControlLeft" && !__isActive(__keys["ControlLeft"][0])) {
__altgr_ctrl_timer = setTimeout(function() {
__altgr_ctrl_timer = null;
self.emitByCode("ControlLeft", true, false);
@@ -165,61 +170,93 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
return true; // Continue handling
};
var __clickHandler = function(el_key, state) {
__commonHandler(el_key, state, false);
__unholdModifiers();
};
var __clickHandler = function(el_key, ev) {
let state = false;
let act = "pressed";
if (ev) {
state = (ev.type === "mousedown" || ev.type === "touchstart");
if (ev.type === "mousedown") {
if (ev.button === 1) {
act = "locked";
} else if (ev.button === 2) {
act = "holded";
}
}
}
var __toggleModifierHandler = function(el_key) {
__commonHandler(el_key, !__isActive(el_key), true);
};
var __commonHandler = function(el_key, state, hold) {
if (state && !__isActive(el_key)) {
__stopHoldTimer(el_key);
__deactivate(el_key);
__activate(el_key, (hold ? "holded" : "pressed"));
__activate(el_key, act);
__process(el_key, true);
__startHoldTimer(el_key);
} else {
__deactivate(el_key);
__process(el_key, false);
let fixed = (__isActive(el_key, "holded") || __isActive(el_key, "locked"));
if (!state && fixed && __stopHoldTimer(el_key)) {
return; // Игнорировать первое отжатие сразу после нажатия
}
if (!state) {
__stopHoldTimer(el_key);
__deactivate(el_key);
__process(el_key, false);
if (!fixed) {
__unholdAll();
}
}
}
};
var __unholdModifiers = function() {
for (let code in __modifiers) {
let el_key = __modifiers[code][0];
if (__isHolded(el_key)) {
var __startHoldTimer = function(el_key) {
__stopHoldTimer(el_key);
let code = el_key.getAttribute("data-code");
__hold_timers[code] = setTimeout(function() {
// Помимо прямой функции, hold timer используется для детектирования факта
// нажатия в рамках одной сессии press/release, чтобы не отпустить сразу же
// зажатую или заблокированную клавишу. Поэтому таймер инициализируется всегда,
// но основную функцию выполняет только если у него есть атрибут data-allow-autohold.
if (el_key.hasAttribute("data-allow-autohold")) {
__deactivate(el_key);
__activate(el_key, "holded");
}
}, 500); // Check keypad.css for the animation
};
var __stopHoldTimer = function(el_key) {
let code = el_key.getAttribute("data-code");
if (!__hold_timers[code]) {
return false;
}
clearTimeout(__hold_timers[code]);
__hold_timers[code] = null;
return true;
};
var __unholdAll = function() {
for (let el_key of [].slice.call(__el_keypad.getElementsByClassName("key"))) {
__stopHoldTimer(el_key);
if (__isActive(el_key, "holded") && !__isActive(el_key, "locked")) { // Skip duplicating keys
__deactivate(el_key);
__process(el_key, false);
}
}
};
var __isPressed = function(el_key) {
let is_pressed = false;
var __isActive = function(el_key, cls=null) {
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
is_pressed = (is_pressed || el_key.classList.contains("pressed"));
if (cls) {
if (el_key.classList.contains(cls)) {
return true;
}
} else if (
el_key.classList.contains("pressed")
|| el_key.classList.contains("holded")
|| el_key.classList.contains("locked")
) {
return true;
}
}
return is_pressed;
};
var __isHolded = function(el_key) {
let is_holded = false;
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
is_holded = (is_holded || el_key.classList.contains("holded"));
}
return is_holded;
};
var __isActive = function(el_key) {
let is_active = false;
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
is_active = (is_active || el_key.classList.contains("pressed") || el_key.classList.contains("holded"));
}
return is_active;
return false;
};
var __activate = function(el_key, cls) {
@@ -234,12 +271,13 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
for (el_key of el_keys) {
el_key.classList.remove("pressed");
el_key.classList.remove("holded");
el_key.classList.remove("locked");
}
};
var __resolveKeys = function(el_key) {
let code = el_key.getAttribute("data-code");
return __merged[code];
return __keys[code];
};
var __process = function(el_key, state) {

View File

@@ -94,7 +94,7 @@ export function Atx(__recorder) {
var __clickAtx = function(button) {
let click_button = function() {
tools.httpPost("/api/atx/click", {"button": button}, function(http) {
tools.httpPost("api/atx/click", {"button": button}, function(http) {
if (http.status === 409) {
wm.error("Performing another ATX operation for other client.<br>Please try again later.");
} else if (http.status !== 200) {
@@ -106,7 +106,7 @@ export function Atx(__recorder) {
if ($("atx-ask-switch").checked) {
wm.confirm(`
Are you sure you want to press the <b>${button}</b> button?<br>
Are you sure you want to press the <b>${tools.escape(button)}</b> button?<br>
Warning! This could cause data loss on the server.
`).then(function(ok) {
if (ok) {

View File

@@ -0,0 +1,78 @@
/*****************************************************************************
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
*****************************************************************************/
"use strict";
import {tools} from "../tools.js";
import {wm} from "../wm.js";
export var clipboard = new function() {
var self = this;
/************************************************************************/
self.setText = function(text) {
let workaround = function(ex) {
// https://stackoverflow.com/questions/60317969/document-execcommandcopy-not-working-even-though-the-dom-element-is-created
wm.info("Press OK to copy the text to the clipboard").then(function() {
tools.error("clipboard.setText(): navigator.clipboard.writeText() is not working:", ex);
tools.info("clipboard.setText(): Trying a workaround...");
let el = document.createElement("textarea");
el.readonly = true;
el.contentEditable = true;
el.style.position = "absolute";
el.style.top = "-1000px";
el.value = text;
document.body.appendChild(el);
// Select the content of the textarea
el.select(); // Ordinary browsers
el.setSelectionRange(0, el.value.length); // iOS
try {
ex = (document.execCommand("copy") ? null : "Unknown error");
} catch (ex) { // eslint-disable-line no-unused-vars
}
// Remove the added textarea again:
document.body.removeChild(el);
if (ex) {
tools.error("clipboard.setText(): Workaround failed:", ex);
wm.error("Can't copy text to the clipboard", `${ex}`);
}
});
};
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
wm.info("The text has been copied to the clipboard");
}, function(ex) {
workaround(ex);
});
} else {
workaround("navigator.clipboard is not available");
}
};
};

View File

@@ -23,6 +23,7 @@
"use strict";
import {ROOT_PREFIX} from "../vars.js";
import {tools, $, $$} from "../tools.js";
import {wm} from "../wm.js";
@@ -133,31 +134,38 @@ export function Gpio(__recorder) {
var __createItem = function(item) {
if (item.type === "label") {
return item.text;
// User may want to use HTML in the text so we don't perform escaping here.
return `<span class="__gpio-label">${item.text}</span>`;
} else if (item.type === "input") {
let e_ch_class = tools.escape(`__gpio-led-${item.channel}`);
let e_icon = tools.escape(`${ROOT_PREFIX}share/svg/led-circle.svg`);
return `
<img
class="__gpio-led __gpio-led-${item.channel} inline-lamp-big led-gray"
src="/share/svg/led-circle.svg"
data-color="${item.color}"
class="__gpio-led ${e_ch_class} inline-lamp-big led-gray"
src="${e_icon}"
data-color="${tools.escape(item.color)}"
/>
`;
} else if (item.type === "output") {
let controls = [];
let confirm = (item.confirm ? "Are you sure you want to perform this action?" : "");
let e_ch = tools.escape(item.channel);
let e_confirm = (item.confirm ? tools.escape("Are you sure you want to perform this action?") : "");
if (item.scheme["switch"]) {
let id = tools.makeId();
let e_id = tools.escape(`__gpio-switch-${tools.makeRandomId()}`);
let e_ch_class = tools.escape(`__gpio-switch-${item.channel}`);
controls.push(`
<td><div class="switch-box">
<input
disabled
type="checkbox"
id="__gpio-switch-${id}"
class="__gpio-switch __gpio-switch-${item.channel}"
data-channel="${item.channel}"
data-confirm="${confirm}"
id="${e_id}"
class="__gpio-switch ${e_ch_class}"
data-channel="${e_ch}"
data-confirm="${e_confirm}"
/>
<label for="__gpio-switch-${id}">
<label for="${e_id}">
<span class="switch-inner"></span>
<span class="switch"></span>
</label>
@@ -165,22 +173,23 @@ export function Gpio(__recorder) {
`);
}
if (item.scheme.pulse.delay) {
let e_ch_class = tools.escape(`__gpio-button-${item.channel}`);
controls.push(`
<td><button
disabled
class="__gpio-button __gpio-button-${item.channel}"
class="__gpio-button ${e_ch_class}"
${item.hide ? "data-force-hide-menu" : ""}
data-channel="${item.channel}"
data-confirm="${confirm}"
data-channel="${e_ch}"
data-confirm="${e_confirm}"
>
${(item.hide ? "&bull; " : "") + item.text}
${(item.hide ? "&bull; " : "") + tools.escape(item.text)}
</button></td>
`);
}
return `<table><tr>${controls.join("<td>&nbsp;&nbsp;&nbsp;</td>")}</tr></table>`;
} else {
return "";
}
return "";
};
var __setLedState = function(el, on) {
@@ -202,7 +211,7 @@ export function Gpio(__recorder) {
confirm = el.getAttribute("data-confirm-off");
}
let act = () => {
__sendPost("/api/gpio/switch", {"channel": ch, "state": to});
__sendPost("api/gpio/switch", {"channel": ch, "state": to});
__recorder.recordGpioSwitchEvent(ch, to);
};
if (confirm) {
@@ -220,7 +229,7 @@ export function Gpio(__recorder) {
let ch = el.getAttribute("data-channel");
let confirm = el.getAttribute("data-confirm");
let act = () => {
__sendPost("/api/gpio/pulse", {"channel": ch});
__sendPost("api/gpio/pulse", {"channel": ch});
__recorder.recordGpioPulseEvent(ch);
};
if (confirm) {

View File

@@ -43,31 +43,11 @@ export function Hid(__getGeometry, __recorder) {
__keyboard = new Keyboard(__recorder.recordWsEvent);
__mouse = new Mouse(__getGeometry, __recorder.recordWsEvent);
let hidden_attr = null;
let visibility_change_attr = null;
if (typeof document.hidden !== "undefined") {
hidden_attr = "hidden";
visibility_change_attr = "visibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden_attr = "webkitHidden";
visibility_change_attr = "webkitvisibilitychange";
} else if (typeof document.mozHidden !== "undefined") {
hidden_attr = "mozHidden";
visibility_change_attr = "mozvisibilitychange";
}
if (visibility_change_attr) {
document.addEventListener(
visibility_change_attr,
function() {
if (document[hidden_attr]) {
__releaseAll();
}
},
false
);
}
document.addEventListener("visibilitychange", function() {
if (document.visibilityState === "hidden") {
__releaseAll();
}
}, false);
window.addEventListener("pagehide", __releaseAll);
window.addEventListener("blur", __releaseAll);
@@ -183,13 +163,13 @@ export function Hid(__getGeometry, __recorder) {
let avail_json = JSON.stringify(avail);
if (el.__avail_json !== avail_json) {
let html = "";
for (let pair of [
for (let kv of [
["USB", "usb"],
["PS/2", "ps2"],
["Off", "disabled"],
]) {
if (avail.includes(pair[1])) {
html += tools.radio.makeItem("hid-outputs-keyboard-radio", pair[0], pair[1]);
if (avail.includes(kv[1])) {
html += tools.radio.makeItem("hid-outputs-keyboard-radio", kv[0], kv[1]);
}
}
el.innerHTML = html;
@@ -211,16 +191,16 @@ export function Hid(__getGeometry, __recorder) {
if (el.__avail_json !== avail_json) {
has_relative = false;
let html = "";
for (let pair of [
for (let kv of [
["Absolute", "usb", false],
["Abs-Win98", "usb_win98", false],
["Relative", "usb_rel", true],
["PS/2", "ps2", true],
["Off", "disabled", false],
]) {
if (avail.includes(pair[1])) {
html += tools.radio.makeItem("hid-outputs-mouse-radio", pair[0], pair[1]);
has_relative = (has_relative || pair[2]);
if (avail.includes(kv[1])) {
html += tools.radio.makeItem("hid-outputs-mouse-radio", kv[0], kv[1]);
has_relative = (has_relative || kv[2]);
}
}
el.innerHTML = html;
@@ -275,7 +255,7 @@ export function Hid(__getGeometry, __recorder) {
var __clickOutputsRadio = function(hid) {
let output = tools.radio.getValue(`hid-outputs-${hid}-radio`);
tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) {
tools.httpPost("api/hid/set_params", {[`${hid}_output`]: output}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure HID", http.responseText);
}
@@ -284,7 +264,7 @@ export function Hid(__getGeometry, __recorder) {
var __clickJigglerSwitch = function() {
let enabled = $("hid-jiggler-switch").checked;
tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) {
tools.httpPost("api/hid/set_params", {"jiggler": enabled}, function(http) {
if (http.status !== 200) {
wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse jiggler`, http.responseText);
}
@@ -293,7 +273,7 @@ export function Hid(__getGeometry, __recorder) {
var __clickConnectSwitch = function() {
let connected = $("hid-connect-switch").checked;
tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) {
tools.httpPost("api/hid/set_connected", {"connected": connected}, function(http) {
if (http.status !== 200) {
wm.error(`Can't ${connected ? "connect" : "disconnect"} HID`, http.responseText);
}
@@ -303,7 +283,7 @@ export function Hid(__getGeometry, __recorder) {
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset HID (keyboard & mouse)?").then(function(ok) {
if (ok) {
tools.httpPost("/api/hid/reset", null, function(http) {
tools.httpPost("api/hid/reset", null, function(http) {
if (http.status !== 200) {
wm.error("HID reset error", http.responseText);
}

275
web/share/js/kvm/info.js Normal file
View File

@@ -0,0 +1,275 @@
/*****************************************************************************
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
*****************************************************************************/
"use strict";
import {ROOT_PREFIX} from "../vars.js";
import {tools, $} from "../tools.js";
export function Info() {
var self = this;
/************************************************************************/
var __health_state = null;
var __fan_state = null;
var __init__ = function() {
};
/************************************************************************/
self.setState = function(state) {
for (let key of Object.keys(state)) {
switch (key) {
case "meta": __setStateMeta(state.meta); break;
case "health": __setStateHealth(state.health); break;
case "fan": __setStateFan(state.fan); break;
case "system": __setStateSystem(state.system); break;
case "extras": __setStateExtras(state.extras); break;
}
}
};
var __setStateMeta = function(state) {
if (state !== null) {
$("kvmd-meta-json").innerText = JSON.stringify(state, undefined, 4);
if (state.server && state.server.host) {
$("kvmd-meta-server-host").innerText = state.server.host;
document.title = `${state.server.host} | PiKVM Session`;
} else {
$("kvmd-meta-server-host").innerText = "";
document.title = "PiKVM Session";
}
for (let place of ["left", "right"]) {
if (state.tips && state.tips[place]) {
$(`kvmd-meta-tips-${place}`).innerText = state.tips[place];
}
}
// Don't use this option, it may be removed in any time
if (state.web && state.web.confirm_session_exit === false) {
window.onbeforeunload = null; // See main.js
}
}
};
var __setStateHealth = function(state) {
if (state.throttling !== null) {
let flags = state.throttling.parsed_flags;
let ignore_past = state.throttling.ignore_past;
let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past));
let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past));
tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped));
$("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden");
$("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden");
tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage);
tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped);
}
__health_state = state;
__renderAboutHardware();
};
var __setStateFan = function(state) {
let failed = false;
let failed_past = false;
if (state.monitored) {
if (state.state === null) {
failed = true;
} else {
if (!state.state.fan.ok) {
failed = true;
} else if (state.state.fan.last_fail_ts >= 0) {
failed = true;
failed_past = true;
}
}
}
tools.hidden.setVisible($("fan-health-dropdown"), failed);
$("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden");
__fan_state = state;
__renderAboutHardware();
};
var __renderAboutHardware = function() {
let parts = [];
if (__health_state !== null) {
parts = [
"Resources:" + __formatMisc(__health_state),
"Temperature:" + __formatTemp(__health_state.temp),
"Throttling:" + __formatThrottling(__health_state.throttling),
];
}
if (__fan_state !== null) {
parts.push("Fan:" + __formatFan(__fan_state));
}
$("about-hardware").innerHTML = parts.join("<hr>");
};
var __formatMisc = function(state) {
return __formatUl([
["CPU", tools.escape(`${state.cpu.percent}%`)],
["MEM", tools.escape(`${state.mem.percent}%`)],
]);
};
var __formatFan = function(state) {
if (!state.monitored) {
return __formatUl([["Status", "Not monitored"]]);
} else if (state.state === null) {
return __formatUl([["Status", __red("Not available")]]);
} else {
state = state.state;
let kvs = [
["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))],
["Desired speed", tools.escape(`${state.fan.speed}%`)],
["PWM", tools.escape(`${state.fan.pwm}`)],
];
if (state.hall.available) {
kvs.push(["RPM", __colored(state.fan.ok, tools.escape(`${state.hall.rpm}`))]);
}
return __formatUl(kvs);
}
};
var __formatTemp = function(temp) {
let kvs = [];
for (let field of Object.keys(temp).sort()) {
kvs.push([
tools.escape(field.toUpperCase()),
tools.escape(`${temp[field]}`) + "&deg;C",
]);
}
return __formatUl(kvs);
};
var __formatThrottling = function(throttling) {
if (throttling !== null) {
let kvs = [];
for (let field of Object.keys(throttling.parsed_flags).sort()) {
let flags = throttling.parsed_flags[field];
let key = tools.upperFirst(field).replace("_", " ");
let value = (flags["now"] ? __red("RIGHT NOW") : __green("No"));
if (!throttling.ignore_past) {
value += "; " + (flags["past"] ? __red("In the past") : __green("Never"));
}
kvs.push([tools.escape(key), value]);
}
return __formatUl(kvs);
} else {
return "NO DATA";
}
};
var __setStateSystem = function(state) {
let p = state.platform;
let s = state.streamer;
$("about-version").innerHTML = `
Base: ${__commented(tools.escape(p.base))}
<hr>
Platform: ${__commented(tools.escape(p.model + "-" + p.video + "-" + p.board))}
<hr>
Serial: ${__commented(tools.escape(p.serial))}
<hr>
KVMD: ${__commented(tools.escape(state.kvmd.version))}
<hr>
Streamer: ${__commented(tools.escape(s.version + " (" + s.app + ")"))}
${__formatStreamerFeatures(s.features)}
<hr>
${tools.escape(state.kernel.system)} kernel:
${__formatUname(state.kernel)}
`;
$("kvmd-info-platform").innerText = p.model;
$("kvmd-version-kvmd").innerText = state.kvmd.version;
$("kvmd-version-streamer").innerText = s.version;
};
var __formatStreamerFeatures = function(features) {
let kvs = [];
for (let field of Object.keys(features).sort()) {
kvs.push([
tools.escape(field),
(features[field] ? "Yes" : "No"),
]);
}
return __formatUl(kvs);
};
var __formatUname = function(kernel) {
let kvs = [];
for (let field of Object.keys(kernel).sort()) {
if (field !== "system") {
kvs.push([
tools.escape(tools.upperFirst(field)),
tools.escape(kernel[field]),
]);
}
}
return __formatUl(kvs);
};
var __formatUl = function(kvs) {
let html = "";
for (let kv of kvs) {
html += `<li>${kv[0]}: ${__commented(kv[1])}</li>`;
}
return `<ul>${html}</ul>`;
};
var __green = (html) => __colored(true, html);
var __red = (html) => __colored(false, html);
var __colored = (ok, html) => `<font color="${ok ? "green" : "red"}">${html}</font>`;
var __commented = (html) => `<span class="code-comment">${html}</span>`;
var __setStateExtras = function(state) {
let show_hook = null;
let close_hook = null;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
if (has_webterm) {
let loc = window.location;
let base = `${loc.protocol}//${loc.host}${loc.pathname}${ROOT_PREFIX}`;
// Tailing slash after state.webterm.path is added to avoid Nginx 301 redirect
// when the location doesn't have tailing slash: "foo -> foo/".
// Reverse proxy over PiKVM can be misconfigured to handle this.
let url = base + state.webterm.path + "/?disableLeaveAlert=true";
show_hook = function() {
tools.info("Terminal opened: ", url);
$("webterm-iframe").src = url;
};
close_hook = function() {
tools.info("Terminal closed");
$("webterm-iframe").src = "";
};
}
tools.feature.setEnabled($("system-tool-webterm"), has_webterm);
$("webterm-window").show_hook = show_hook;
$("webterm-window").close_hook = close_hook;
};
__init__();
}

View File

@@ -35,17 +35,17 @@ export function Keyboard(__recordWsEvent) {
var __keypad = null;
var __init__ = function() {
__keypad = new Keypad("div#keyboard-window", __sendKey, true);
__keypad = new Keypad($("keyboard-window"), __sendKey, true);
$("hid-keyboard-led").title = "Keyboard free";
$("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true);
$("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false);
$("keyboard-window").onkeydown = (ev) => __keyboardHandler(ev, true);
$("keyboard-window").onkeyup = (ev) => __keyboardHandler(ev, false);
$("keyboard-window").onfocus = __updateOnlineLeds;
$("keyboard-window").onblur = __updateOnlineLeds;
$("stream-window").onkeydown = (event) => __keyboardHandler(event, true);
$("stream-window").onkeyup = (event) => __keyboardHandler(event, false);
$("stream-window").onkeydown = (ev) => __keyboardHandler(ev, true);
$("stream-window").onkeyup = (ev) => __keyboardHandler(ev, false);
$("stream-window").onfocus = __updateOnlineLeds;
$("stream-window").onblur = __updateOnlineLeds;
@@ -125,9 +125,9 @@ export function Keyboard(__recordWsEvent) {
$("hid-keyboard-led").title = title;
};
var __keyboardHandler = function(event, state) {
event.preventDefault();
__keypad.emitByKeyEvent(event, state);
var __keyboardHandler = function(ev, state) {
ev.preventDefault();
__keypad.emitByKeyEvent(ev, state);
};
var __sendKey = function(code, state) {
@@ -139,7 +139,7 @@ export function Keyboard(__recordWsEvent) {
code = "ControlLeft";
}
}
let event = {
let ev = {
"event_type": "key",
"event": {
"key": code,
@@ -148,10 +148,10 @@ export function Keyboard(__recordWsEvent) {
},
};
if (__ws && !$("hid-mute-switch").checked) {
__ws.sendHidEvent(event);
__ws.sendHidEvent(ev);
}
delete event.event.finish;
__recordWsEvent(event);
delete ev.event.finish;
__recordWsEvent(ev);
};
__init__();

View File

@@ -32,7 +32,7 @@ import {Session} from "./session.js";
export function main() {
if (checkBrowser(null, "/share/css/kvm/x-mobile.css")) {
if (checkBrowser(null, "kvm/x-mobile.css")) {
tools.storage.bindSimpleSwitch($("page-close-ask-switch"), "page.close.ask", true, function(value) {
if (value) {
window.onbeforeunload = function(event) {
@@ -49,7 +49,7 @@ export function main() {
initWindowManager();
tools.el.setOnClick($("open-log-button"), () => window.open("/api/log?seek=3600&follow=1", "_blank"));
tools.el.setOnClick($("open-log-button"), () => tools.windowOpen("api/log?seek=3600&follow=1"));
tools.storage.bindSimpleSwitch(
$("page-full-tab-stream-switch"),

View File

@@ -34,38 +34,43 @@ export function Mouse(__getGeometry, __recordWsEvent) {
var __ws = null;
var __online = true;
var __absolute = true;
var __abs = true;
var __keypad = null;
var __timer = null;
var __planned_pos = {"x": 0, "y": 0};
var __sent_pos = {"x": 0, "y": 0};
var __relative_deltas = [];
var __relative_touch_pos = null;
var __relative_sens = 1.0;
var __touch_pos = null;
var __abs_pos = null;
var __rel_sens = 1.0;
var __rel_deltas = [];
var __scroll_rate = 5;
var __scroll_fix = (tools.browser.is_mac ? 5 : 1);
var __scroll_delta = {"x": 0, "y": 0};
var __stream_hovered = false;
var __init__ = function() {
__keypad = new Keypad("div#stream-mouse-buttons", __sendButton, false);
__keypad = new Keypad($("stream-mouse-buttons"), __sendButton, false);
$("hid-mouse-led").title = "Mouse free";
document.onpointerlockchange = __relativeCapturedHandler; // Only for relative
document.onpointerlockerror = __relativeCapturedHandler;
$("stream-box").onmouseenter = () => __streamHoveredHandler(true);
$("stream-box").onmouseleave = () => __streamHoveredHandler(false);
$("stream-box").onmousedown = (event) => __streamButtonHandler(event, true);
$("stream-box").onmouseup = (event) => __streamButtonHandler(event, false);
$("stream-box").oncontextmenu = (event) => event.preventDefault();
$("stream-box").onmousemove = __streamMoveHandler;
$("stream-box").onwheel = __streamScrollHandler;
$("stream-box").ontouchstart = (event) => __streamTouchStartHandler(event);
$("stream-box").ontouchmove = (event) => __streamTouchMoveHandler(event);
$("stream-box").ontouchend = (event) => __streamTouchEndHandler(event);
document.addEventListener("pointerlockchange", __relativeCapturedHandler); // Only for relative
document.addEventListener("pointerlockerror", __relativeCapturedHandler);
$("stream-box").addEventListener("contextmenu", (ev) => ev.preventDefault());
$("stream-box").addEventListener("mouseenter", () => __streamHoveredHandler(true));
$("stream-box").addEventListener("mouseleave", () => __streamHoveredHandler(false));
$("stream-box").addEventListener("mousedown", (ev) => __streamButtonHandler(ev, true));
$("stream-box").addEventListener("mouseup", (ev) => __streamButtonHandler(ev, false));
$("stream-box").addEventListener("mousemove", __streamMoveHandler);
$("stream-box").addEventListener("wheel", __streamScrollHandler);
$("stream-box").addEventListener("touchstart", __streamTouchStartHandler);
$("stream-box").addEventListener("touchmove", __streamTouchMoveHandler);
$("stream-box").addEventListener("touchend", __streamTouchEndHandler);
tools.storage.bindSimpleSwitch($("hid-mouse-squash-switch"), "hid.mouse.squash", true);
tools.slider.setParams($("hid-mouse-sens-slider"), 0.1, 1.9, 0.1, tools.storage.get("hid.mouse.sens", 1.0), __updateRelativeSens);
@@ -84,26 +89,26 @@ export function Mouse(__getGeometry, __recordWsEvent) {
self.setSocket = function(ws) {
__ws = ws;
if (!__absolute && __isRelativeCaptured()) {
if (!__abs && __isRelativeCaptured()) {
document.exitPointerLock();
}
__updateOnlineLeds();
};
self.setState = function(online, absolute, hid_online, hid_busy) {
self.setState = function(online, abs, hid_online, hid_busy) {
if (!hid_online) {
__online = null;
} else {
__online = (online && !hid_busy);
}
if (!__absolute && absolute && __isRelativeCaptured()) {
if (!__abs && abs && __isRelativeCaptured()) {
document.exitPointerLock();
}
if (__absolute && !absolute) {
__relative_deltas = [];
__relative_touch_pos = null;
if (__abs && !abs) {
__touch_pos = null;
__rel_deltas = [];
}
__absolute = absolute;
__abs = abs;
__updateOnlineLeds();
};
@@ -112,7 +117,7 @@ export function Mouse(__getGeometry, __recordWsEvent) {
};
var __updateRate = function(value) {
$("hid-mouse-rate-value").innerHTML = value + " ms";
$("hid-mouse-rate-value").innerText = value + " ms";
tools.storage.set("hid.mouse.rate", value);
if (__timer) {
clearInterval(__timer);
@@ -121,19 +126,19 @@ export function Mouse(__getGeometry, __recordWsEvent) {
};
var __updateScrollRate = function(value) {
$("hid-mouse-scroll-value").innerHTML = value;
$("hid-mouse-scroll-value").innerText = value;
tools.storage.set("hid.mouse.scroll_rate", value);
__scroll_rate = value;
};
var __updateRelativeSens = function(value) {
$("hid-mouse-sens-value").innerHTML = value.toFixed(1);
$("hid-mouse-sens-value").innerText = value.toFixed(1);
tools.storage.set("hid.mouse.sens", value);
__relative_sens = value;
__rel_sens = value;
};
var __streamHoveredHandler = function(hovered) {
if (__absolute) {
if (__abs) {
__stream_hovered = hovered;
__updateOnlineLeds();
}
@@ -141,7 +146,7 @@ export function Mouse(__getGeometry, __recordWsEvent) {
var __updateOnlineLeds = function() {
let is_captured;
if (__absolute) {
if (__abs) {
is_captured = (__stream_hovered || tools.browser.is_mobile);
} else {
is_captured = __isRelativeCaptured();
@@ -170,7 +175,7 @@ export function Mouse(__getGeometry, __recordWsEvent) {
$("hid-mouse-led").className = led;
$("hid-mouse-led").title = title;
if (__absolute && is_captured) {
if (__abs && is_captured) {
let dot = $("hid-mouse-dot-switch").checked;
$("stream-box").classList.toggle("stream-box-mouse-dot", (dot && __ws));
$("stream-box").classList.toggle("stream-box-mouse-none", (!dot && __ws));
@@ -189,128 +194,149 @@ export function Mouse(__getGeometry, __recordWsEvent) {
__updateOnlineLeds();
};
var __streamButtonHandler = function(event, state) {
var __streamButtonHandler = function(ev, state) {
// https://www.w3schools.com/jsref/event_button.asp
event.preventDefault();
if (__absolute || __isRelativeCaptured()) {
switch (event.button) {
ev.preventDefault();
if (__abs || __isRelativeCaptured()) {
switch (ev.button) {
case 0: __keypad.emitByCode("left", state); break;
case 2: __keypad.emitByCode("right", state); break;
case 1: __keypad.emitByCode("middle", state); break;
case 3: __keypad.emitByCode("up", state); break;
case 4: __keypad.emitByCode("down", state); break;
}
} else if (!__absolute && !__isRelativeCaptured() && !state) {
} else if (!__abs && !__isRelativeCaptured() && !state) {
$("stream-box").requestPointerLock();
}
};
var __streamTouchStartHandler = function(event) {
event.preventDefault();
if (event.touches.length === 1) {
if (__absolute) {
__planned_pos = __getTouchPosition(event, 0);
__sendPlannedMove();
} else {
__relative_touch_pos = __getTouchPosition(event, 0);
}
var __streamTouchStartHandler = function(ev) {
ev.preventDefault();
let pos = __getTouchPosition(ev, 0);
if (__abs && ev.touches.length === 1) {
__abs_pos = pos;
__sendPlannedMove();
} else if (!__abs) {
__touch_pos = pos;
__abs_pos = null;
}
};
var __streamTouchMoveHandler = function(event) {
event.preventDefault();
if (event.touches.length === 1) {
if (__absolute) {
__planned_pos = __getTouchPosition(event, 0);
} else if (__relative_touch_pos === null) {
__relative_touch_pos = __getTouchPosition(event, 0);
} else {
let pos = __getTouchPosition(event, 0);
var __streamTouchMoveHandler = function(ev) {
ev.preventDefault();
let pos = __getTouchPosition(ev, 0);
if (ev.touches.length === 1) {
if (__abs) {
__abs_pos = pos;
} else if (__touch_pos !== null) {
__sendOrPlanRelativeMove({
"x": (pos.x - __relative_touch_pos.x),
"y": (pos.y - __relative_touch_pos.y),
"x": (pos.x - __touch_pos.x),
"y": (pos.y - __touch_pos.y),
});
__relative_touch_pos = pos;
__touch_pos = pos;
}
} else if (ev.touches.length >= 2) {
if (__touch_pos === null) {
__touch_pos = pos;
} else {
let dx = __touch_pos.x - pos.x;
let dy = __touch_pos.y - pos.y;
if (Math.abs(dx) < 15) {
dx = 0;
}
if (Math.abs(dy) < 15) {
dy = 0;
}
if (dx || dy) {
__sendScroll({"x": dx, "y": dy});
__touch_pos = null;
}
}
__abs_pos = null;
}
};
var __streamTouchEndHandler = function(event) {
event.preventDefault();
var __streamTouchEndHandler = function(ev) {
ev.preventDefault();
__sendPlannedMove();
__touch_pos = null;
__abs_pos = null;
};
var __getTouchPosition = function(event, index) {
if (event.touches[index].target && event.touches[index].target.getBoundingClientRect) {
let rect = event.touches[index].target.getBoundingClientRect();
var __getTouchPosition = function(ev, index) {
if (ev.touches[index].target && ev.touches[index].target.getBoundingClientRect) {
let rect = ev.touches[index].target.getBoundingClientRect();
return {
"x": Math.round(event.touches[index].clientX - rect.left),
"y": Math.round(event.touches[index].clientY - rect.top),
"x": Math.round(ev.touches[index].clientX - rect.left),
"y": Math.round(ev.touches[index].clientY - rect.top),
};
}
return null;
};
var __streamMoveHandler = function(event) {
if (__absolute) {
let rect = event.target.getBoundingClientRect();
__planned_pos = {
"x": Math.max(Math.round(event.clientX - rect.left), 0),
"y": Math.max(Math.round(event.clientY - rect.top), 0),
var __streamMoveHandler = function(ev) {
if (__abs) {
let rect = ev.target.getBoundingClientRect();
__abs_pos = {
"x": Math.max(Math.round(ev.clientX - rect.left), 0),
"y": Math.max(Math.round(ev.clientY - rect.top), 0),
};
} else if (__isRelativeCaptured()) {
__sendOrPlanRelativeMove({
"x": event.movementX,
"y": event.movementY,
"x": ev.movementX,
"y": ev.movementY,
});
}
};
var __streamScrollHandler = function(event) {
var __streamScrollHandler = function(ev) {
// https://learn.javascript.ru/mousewheel
// https://stackoverflow.com/a/24595588
event.preventDefault();
ev.preventDefault();
if (!__absolute && !__isRelativeCaptured()) {
if (!__abs && !__isRelativeCaptured()) {
return;
}
let delta = {"x": 0, "y": 0};
if ($("hid-mouse-cumulative-scrolling-switch").checked) {
let factor = (tools.browser.is_mac ? 5 : 1);
__scroll_delta.x += event.deltaX * factor; // Horizontal scrolling
if (Math.abs(__scroll_delta.x) >= 100) {
delta.x = __scroll_delta.x / Math.abs(__scroll_delta.x) * (-__scroll_rate);
if (__scroll_delta.x && Math.sign(__scroll_delta.x) !== Math.sign(ev.deltaX)) {
delta.x = __scroll_delta.x;
__scroll_delta.x = 0;
} else {
__scroll_delta.x += ev.deltaX * __scroll_fix;
if (Math.abs(__scroll_delta.x) >= 100) {
delta.x = __scroll_delta.x;
__scroll_delta.x = 0;
}
}
__scroll_delta.y += event.deltaY * factor; // Vertical scrolling
if (Math.abs(__scroll_delta.y) >= 100) {
delta.y = __scroll_delta.y / Math.abs(__scroll_delta.y) * (-__scroll_rate);
if (__scroll_delta.y && Math.sign(__scroll_delta.y) !== Math.sign(ev.deltaY)) {
delta.y = __scroll_delta.y;
__scroll_delta.y = 0;
} else {
__scroll_delta.y += ev.deltaY * __scroll_fix;
if (Math.abs(__scroll_delta.y) >= 100) {
delta.y = __scroll_delta.y;
__scroll_delta.y = 0;
}
}
} else {
if (event.deltaX !== 0) {
delta.x = event.deltaX / Math.abs(event.deltaX) * (-__scroll_rate);
}
if (event.deltaY !== 0) {
delta.y = event.deltaY / Math.abs(event.deltaY) * (-__scroll_rate);
}
delta.x = ev.deltaX;
delta.y = ev.deltaY;
}
__sendScroll(delta);
};
var __sendOrPlanRelativeMove = function(delta) {
delta = {
"x": Math.min(Math.max(-127, Math.floor(delta.x * __relative_sens)), 127),
"y": Math.min(Math.max(-127, Math.floor(delta.y * __relative_sens)), 127),
"x": Math.min(Math.max(-127, Math.floor(delta.x * __rel_sens)), 127),
"y": Math.min(Math.max(-127, Math.floor(delta.y * __rel_sens)), 127),
};
if (delta.x || delta.y) {
if ($("hid-mouse-squash-switch").checked) {
__relative_deltas.push(delta);
__rel_deltas.push(delta);
} else {
tools.debug("Mouse: relative:", delta);
__sendEvent("mouse_relative", {"delta": delta});
@@ -319,35 +345,41 @@ export function Mouse(__getGeometry, __recordWsEvent) {
};
var __sendScroll = function(delta) {
if (delta.x || delta.y) {
if ($("hid-mouse-reverse-scrolling-switch").checked) {
delta.y *= -1;
}
// Send a single scroll step defined by rate
if (delta.x) {
delta.x = Math.sign(delta.x) * (-__scroll_rate);
if ($("hid-mouse-reverse-panning-switch").checked) {
delta.x *= -1;
}
}
if (delta.y) {
delta.y = Math.sign(delta.y) * (-__scroll_rate);
if ($("hid-mouse-reverse-scrolling-switch").checked) {
delta.y *= -1;
}
}
if (delta.x || delta.y) {
tools.debug("Mouse: scrolled:", delta);
__sendEvent("mouse_wheel", {"delta": delta});
}
};
var __sendPlannedMove = function() {
if (__absolute) {
let pos = __planned_pos;
if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
if (__abs) {
if (__abs_pos !== null) {
let geo = __getGeometry();
let to = {
"x": tools.remap(pos.x, geo.x, geo.width, -32768, 32767),
"y": tools.remap(pos.y, geo.y, geo.height, -32768, 32767),
"x": tools.remap(__abs_pos.x - geo.x, 0, geo.width - 1, -32768, 32767),
"y": tools.remap(__abs_pos.y - geo.y, 0, geo.height - 1, -32768, 32767),
};
tools.debug("Mouse: moved:", to);
tools.debug("Mouse: abs:", to);
__sendEvent("mouse_move", {"to": to});
__sent_pos = pos;
__abs_pos = null;
}
} else if (__relative_deltas.length) {
tools.debug("Mouse: relative:", __relative_deltas);
__sendEvent("mouse_relative", {"delta": __relative_deltas, "squash": true});
__relative_deltas = [];
} else if (__rel_deltas.length) {
tools.debug("Mouse: relative:", __rel_deltas);
__sendEvent("mouse_relative", {"delta": __rel_deltas, "squash": true});
__rel_deltas = [];
}
};
@@ -357,12 +389,12 @@ export function Mouse(__getGeometry, __recordWsEvent) {
__sendEvent("mouse_button", {"button": button, "state": state});
};
var __sendEvent = function(event_type, event) {
event = {"event_type": event_type, "event": event};
var __sendEvent = function(ev_type, ev) {
ev = {"event_type": ev_type, "event": ev};
if (__ws && !$("hid-mute-switch").checked) {
__ws.sendHidEvent(event);
__ws.sendHidEvent(ev);
}
__recordWsEvent(event);
__recordWsEvent(ev);
};
__init__();

View File

@@ -24,6 +24,7 @@
"use strict";
import {ROOT_PREFIX} from "../vars.js";
import {tools, $} from "../tools.js";
import {wm} from "../wm.js";
@@ -226,7 +227,7 @@ export function Msd() {
if (el.__names_json !== names_json) {
el.innerHTML = names.map(name => `
<div class="text">
<div id="__msd-storage-${tools.makeIdByText(name)}-progress" class="progress">
<div id="__msd-storage-${tools.makeTextId(name)}-progress" class="progress">
<span class="progress-value"></span>
</div>
</div>
@@ -241,7 +242,7 @@ export function Msd() {
? `${names.length === 1 ? "Storage: %s" : "Internal storage: %s"}` // eslint-disable-line
: `Storage [${name}${part.writable ? "]" : ", read-only]"}: %s` // eslint-disable-line
);
let id = `__msd-storage-${tools.makeIdByText(name)}-progress`;
let id = `__msd-storage-${tools.makeTextId(name)}-progress`;
tools.progress.setSizeOf($(id), title, part.size, part.free);
}
};
@@ -291,15 +292,15 @@ export function Msd() {
};
var __clickDownloadButton = function() {
let image = encodeURIComponent($("msd-image-selector").value);
window.open(`/api/msd/read?image=${image}`);
let e_image = encodeURIComponent($("msd-image-selector").value);
tools.windowOpen(`api/msd/read?image=${e_image}`);
};
var __clickRemoveButton = function() {
let name = $("msd-image-selector").value;
wm.confirm("Are you sure you want to remove this image?", name).then(function(ok) {
if (ok) {
tools.httpPost("/api/msd/remove", {"image": name}, function(http) {
tools.httpPost("api/msd/remove", {"image": name}, function(http) {
if (http.status !== 200) {
wm.error("Can't remove image", http.responseText);
}
@@ -309,7 +310,7 @@ export function Msd() {
};
var __sendParam = function(name, value) {
tools.httpPost("/api/msd/set_params", {[name]: value}, function(http) {
tools.httpPost("api/msd/set_params", {[name]: value}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure Mass Storage", http.responseText);
}
@@ -335,11 +336,11 @@ export function Msd() {
prefix = __state.storage.filespath;
}
if (file) {
let image = encodeURIComponent(file.name);
__http.open("POST", `/api/msd/write?prefix=${prefix}&image=${image}&remove_incomplete=1`, true);
let e_image = encodeURIComponent(file.name);
__http.open("POST", `${ROOT_PREFIX}api/msd/write?prefix=${e_prefix}&image=${e_image}&remove_incomplete=1`, true);
} else {
let url = encodeURIComponent($("msd-new-url").value);
__http.open("POST", `/api/msd/write_remote?prefix=${prefix}&url=${url}&remove_incomplete=1`, true);
let e_url = encodeURIComponent($("msd-new-url").value);
__http.open("POST", `${ROOT_PREFIX}api/msd/write_remote?prefix=${e_prefix}&url=${e_url}&remove_incomplete=1`, true);
}
__http.upload.timeout = 7 * 24 * 3600;
__http.onreadystatechange = __uploadStateChange;
@@ -395,7 +396,7 @@ export function Msd() {
};
var __clickConnectButton = function(connected) {
tools.httpPost("/api/msd/set_connected", {"connected": connected}, function(http) {
tools.httpPost("api/msd/set_connected", {"connected": connected}, function(http) {
if (http.status !== 200) {
wm.error("Can't switch Mass Storage", http.responseText);
}
@@ -423,7 +424,7 @@ export function Msd() {
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset Mass Storage?").then(function(ok) {
if (ok) {
tools.httpPost("/api/msd/reset", null, function(http) {
tools.httpPost("api/msd/reset", null, function(http) {
if (http.status !== 200) {
wm.error("Mass Storage reset error", http.responseText);
}
@@ -451,7 +452,8 @@ export function Msd() {
if (__state && __state.storage && __state.storage.parts) {
let part = __state.storage.parts[$("msd-new-part-selector").value];
if (part && (file.size > part.size)) {
wm.error(`The new image is too big for the Mass Storage partition.<br>Maximum: ${tools.formatSize(part.size)}`);
let e_size = tools.escape(tools.formatSize(part.size));
wm.error(`The new image is too big for the Mass Storage partition.<br>Maximum: ${e_size}`);
el.value = "";
}
}

View File

@@ -25,6 +25,7 @@
import {tools, $} from "../tools.js";
import {wm} from "../wm.js";
import {clipboard} from "./clipboard.js";
export function Ocr(__getGeometry) {
@@ -53,14 +54,14 @@ export function Ocr(__getGeometry) {
$("stream-ocr-window").addEventListener("resize", __resetSelection);
$("stream-ocr-window").close_hook = __resetSelection;
$("stream-ocr-window").onkeyup = function(event) {
event.preventDefault();
if (event.code === "Enter") {
$("stream-ocr-window").onkeyup = function(ev) {
ev.preventDefault();
if (ev.code === "Enter") {
if (__sel) {
__recognizeSelection();
wm.closeWindow($("stream-ocr-window"));
}
} else if (event.code === "Escape") {
} else if (ev.code === "Escape") {
wm.closeWindow($("stream-ocr-window"));
}
};
@@ -98,17 +99,17 @@ export function Ocr(__getGeometry) {
el.value = tools.storage.get("stream.ocr.lang", langs["default"]);
};
var __startSelection = function(event) {
var __startSelection = function(ev) {
if (__start_pos === null) {
tools.hidden.setVisible($("stream-ocr-selection"), false);
__start_pos = __getGlobalPosition(event);
__start_pos = __getGlobalPosition(ev);
__end_pos = null;
}
};
var __changeSelection = function(event) {
var __changeSelection = function(ev) {
if (__start_pos !== null) {
__end_pos = __getGlobalPosition(event);
__end_pos = __getGlobalPosition(ev);
let width = Math.abs(__start_pos.x - __end_pos.x);
let height = Math.abs(__start_pos.y - __end_pos.y);
let el = $("stream-ocr-selection");
@@ -120,8 +121,8 @@ export function Ocr(__getGeometry) {
}
};
var __endSelection = function(event) {
__changeSelection(event);
var __endSelection = function(ev) {
__changeSelection(ev);
let el = $("stream-ocr-selection");
let ok = (
el.offsetWidth > 1 && el.offsetHeight > 1
@@ -137,10 +138,10 @@ export function Ocr(__getGeometry) {
let rel_bottom = Math.max(__start_pos.y, __end_pos.y) - rect.top + offset;
let geo = __getGeometry();
__sel = {
"left": tools.remap(rel_left, geo.x, geo.width, 0, geo.real_width),
"right": tools.remap(rel_right, geo.x, geo.width, 0, geo.real_width),
"top": tools.remap(rel_top, geo.y, geo.height, 0, geo.real_height),
"bottom": tools.remap(rel_bottom, geo.y, geo.height, 0, geo.real_height),
"left": tools.remap(rel_left - geo.x, 0, geo.width, 0, geo.real_width),
"right": tools.remap(rel_right - geo.x, 0, geo.width, 0, geo.real_width),
"top": tools.remap(rel_top - geo.y, 0, geo.height, 0, geo.real_height),
"bottom": tools.remap(rel_bottom - geo.y, 0, geo.height, 0, geo.real_height),
};
} else {
__sel = null;
@@ -149,13 +150,13 @@ export function Ocr(__getGeometry) {
__end_pos = null;
};
var __getGlobalPosition = function(event) {
var __getGlobalPosition = function(ev) {
let rect = $("stream-box").getBoundingClientRect();
let geo = __getGeometry();
let offset = __getNavbarOffset();
return {
"x": Math.min(Math.max(event.clientX, rect.left + geo.x), rect.right - geo.x),
"y": Math.min(Math.max(event.clientY - offset, rect.top + geo.y - offset), rect.bottom - geo.y - offset),
"x": Math.min(Math.max(ev.clientX, rect.left + geo.x), rect.right - geo.x),
"y": Math.min(Math.max(ev.clientY - offset, rect.top + geo.y - offset), rect.bottom - geo.y - offset),
};
};
@@ -179,6 +180,7 @@ export function Ocr(__getGeometry) {
tools.el.setEnabled($("stream-ocr-lang-selector"), false);
$("stream-ocr-led").className = "led-yellow-rotating-fast";
let params = {
"allow_offline": 1,
"ocr": 1,
"ocr_langs": $("stream-ocr-lang-selector").value,
"ocr_left": __sel.left,
@@ -186,9 +188,9 @@ export function Ocr(__getGeometry) {
"ocr_right": __sel.right,
"ocr_bottom": __sel.bottom,
};
tools.httpGet("/api/streamer/snapshot", params, function(http) {
tools.httpGet("api/streamer/snapshot", params, function(http) {
if (http.status === 200) {
wm.copyTextToClipboard(http.responseText);
clipboard.setText(http.responseText);
} else {
wm.error("OCR error:<br>", http.responseText);
}

View File

@@ -33,14 +33,27 @@ export function Paste(__recorder) {
/************************************************************************/
var __init__ = function() {
$("hid-pak-text").addEventListener("keyup", function(ev) {
if (ev.ctrlKey && ev.code == "Enter") {
$("hid-pak-button").click();
}
});
tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true);
tools.storage.bindSimpleSwitch($("hid-pak-slow-switch"), "hid.pak.slow", false);
tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) {
$("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none"));
});
tools.slider.setParams($("hid-pak-delay-slider"), 0, 200, 20, tools.storage.getInt("hid.pak.delay", 20), function (value) {
$("hid-pak-delay-value").innerText = value + " ms";
tools.storage.setInt("hid.pak.delay", value);
});
$("hid-pak-keymap-selector").addEventListener("change", function() {
tools.storage.set("hid.pak.keymap", $("hid-pak-keymap-selector").value);
});
tools.el.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton);
};
@@ -68,11 +81,11 @@ export function Paste(__recorder) {
tools.el.setEnabled($("hid-pak-keymap-selector"), false);
let keymap = $("hid-pak-keymap-selector").value;
let slow = $("hid-pak-slow-switch").checked;
let delay = $("hid-pak-delay-slider").value;
tools.debug(`HID: paste-as-keys ${keymap}: ${text}`);
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap, "slow": slow}, function(http) {
tools.httpPost("api/hid/print", {"limit": 0, "keymap": keymap, "delay": delay / 1000}, function(http) {
tools.el.setEnabled($("hid-pak-text"), true);
tools.el.setEnabled($("hid-pak-button"), true);
tools.el.setEnabled($("hid-pak-keymap-selector"), true);
@@ -82,9 +95,9 @@ export function Paste(__recorder) {
} else if (http.status !== 200) {
wm.error("HID paste error", http.responseText);
} else if (http.status === 200) {
__recorder.recordPrintEvent(text, keymap, slow);
__recorder.recordPrintEvent(text, keymap, delay);
}
}, text, "text/plain");
}, text, "text/plain", 7 * 24 * 3600);
};
if ($("hid-pak-ask-switch").checked) {

View File

@@ -63,12 +63,12 @@ export function Recorder() {
__refresh();
};
self.recordWsEvent = function(event) {
__recordEvent(event);
self.recordWsEvent = function(ev) {
__recordEvent(ev);
};
self.recordPrintEvent = function(text, keymap, slow) {
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap, "slow": slow}});
self.recordPrintEvent = function(text, keymap, delay) {
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap, "delay": delay}});
};
self.recordAtxButtonEvent = function(button) {
@@ -83,7 +83,7 @@ export function Recorder() {
__recordEvent({"event_type": "gpio_pulse", "event": {"channel": channel}});
};
var __recordEvent = function(event) {
var __recordEvent = function(ev) {
if (__recording) {
let now = new Date().getTime();
if (__last_event_ts) {
@@ -92,7 +92,7 @@ export function Recorder() {
__events_time += delay;
}
__last_event_ts = now;
__events.push(event);
__events.push(ev);
__setCounters(__events.length, __events_time);
}
};
@@ -149,73 +149,76 @@ export function Recorder() {
let raw_events = JSON.parse(reader.result);
__checkType(raw_events, "object", "Base of script is not an objects list");
for (let event of raw_events) {
__checkType(event, "object", "Non-dict event");
__checkType(event.event, "object", "Non-dict event");
for (let ev of raw_events) {
__checkType(ev, "object", "Non-dict event");
__checkType(ev.event, "object", "Non-dict event");
if (event.event_type === "delay") {
__checkUnsigned(event.event.millis, "Non-unsigned delay");
events_time += event.event.millis;
if (ev.event_type === "delay") {
__checkUnsigned(ev.event.millis, "Non-unsigned delay");
events_time += ev.event.millis;
} else if (event.event_type === "print") {
__checkType(event.event.text, "string", "Non-string print text");
if (event.event.keymap !== undefined) {
__checkType(event.event.keymap, "string", "Non-string keymap");
} else if (ev.event_type === "print") {
__checkType(ev.event.text, "string", "Non-string print text");
if (ev.event.keymap !== undefined) {
__checkType(ev.event.keymap, "string", "Non-string keymap");
}
if (event.event.slow !== undefined) {
__checkType(event.event.slow, "boolean", "Non-bool slow");
if (ev.event.slow !== undefined) {
__checkType(ev.event.slow, "boolean", "Non-bool slow");
}
if (ev.event.delay !== undefined) {
__checkInt(ev.event.delay, "Non-int delay");
}
} else if (event.event_type === "key") {
__checkType(event.event.key, "string", "Non-string key code");
__checkType(event.event.state, "boolean", "Non-bool key state");
} else if (ev.event_type === "key") {
__checkType(ev.event.key, "string", "Non-string key code");
__checkType(ev.event.state, "boolean", "Non-bool key state");
} else if (event.event_type === "mouse_button") {
__checkType(event.event.button, "string", "Non-string mouse button code");
__checkType(event.event.state, "boolean", "Non-bool mouse button state");
} else if (ev.event_type === "mouse_button") {
__checkType(ev.event.button, "string", "Non-string mouse button code");
__checkType(ev.event.state, "boolean", "Non-bool mouse button state");
} else if (event.event_type === "mouse_move") {
__checkType(event.event.to, "object", "Non-object mouse move target");
__checkInt(event.event.to.x, "Non-int mouse move X");
__checkInt(event.event.to.y, "Non-int mouse move Y");
} else if (ev.event_type === "mouse_move") {
__checkType(ev.event.to, "object", "Non-object mouse move target");
__checkInt(ev.event.to.x, "Non-int mouse move X");
__checkInt(ev.event.to.y, "Non-int mouse move Y");
} else if (event.event_type === "mouse_relative") {
__checkMouseRelativeDelta(event.event.delta);
__checkType(event.event.squash, "boolean", "Non-boolean squash");
} else if (ev.event_type === "mouse_relative") {
__checkMouseRelativeDelta(ev.event.delta);
__checkType(ev.event.squash, "boolean", "Non-boolean squash");
} else if (event.event_type === "mouse_wheel") {
__checkType(event.event.delta, "object", "Non-object mouse wheel delta");
__checkInt(event.event.delta.x, "Non-int mouse delta X");
__checkInt(event.event.delta.y, "Non-int mouse delta Y");
} else if (ev.event_type === "mouse_wheel") {
__checkType(ev.event.delta, "object", "Non-object mouse wheel delta");
__checkInt(ev.event.delta.x, "Non-int mouse delta X");
__checkInt(ev.event.delta.y, "Non-int mouse delta Y");
} else if (event.event_type === "atx_button") {
__checkType(event.event.button, "string", "Non-string ATX button");
} else if (ev.event_type === "atx_button") {
__checkType(ev.event.button, "string", "Non-string ATX button");
} else if (event.event_type === "gpio_switch") {
__checkType(event.event.channel, "string", "Non-string GPIO channel");
__checkType(event.event.state, "boolean", "Non-bool GPIO state");
} else if (ev.event_type === "gpio_switch") {
__checkType(ev.event.channel, "string", "Non-string GPIO channel");
__checkType(ev.event.state, "boolean", "Non-bool GPIO state");
} else if (event.event_type === "gpio_pulse") {
__checkType(event.event.channel, "string", "Non-string GPIO channel");
} else if (ev.event_type === "gpio_pulse") {
__checkType(ev.event.channel, "string", "Non-string GPIO channel");
} else if (event.event_type === "delay_random") {
__checkType(event.event.range, "object", "Non-object random delay range");
__checkUnsigned(event.event.range.min, "Non-unsigned random delay range min");
__checkUnsigned(event.event.range.max, "Non-unsigned random delay range max");
__checkRangeMinMax(event.event.range, "Invalid random delay range");
events_time += event.event.range.max;
} else if (ev.event_type === "delay_random") {
__checkType(ev.event.range, "object", "Non-object random delay range");
__checkUnsigned(ev.event.range.min, "Non-unsigned random delay range min");
__checkUnsigned(ev.event.range.max, "Non-unsigned random delay range max");
__checkRangeMinMax(ev.event.range, "Invalid random delay range");
events_time += ev.event.range.max;
} else if (event.event_type === "mouse_move_random") { // Hack for pikvm/pikvm#1041
__checkType(event.event.range, "object", "Non-object random mouse move range");
__checkInt(event.event.range.min, "Non-int random mouse move range min");
__checkInt(event.event.range.max, "Non-int random mouse move range max");
__checkRangeMinMax(event.event.range, "Invalid random mouse move range");
} else if (ev.event_type === "mouse_move_random") { // Hack for pikvm/pikvm#1041
__checkType(ev.event.range, "object", "Non-object random mouse move range");
__checkInt(ev.event.range.min, "Non-int random mouse move range min");
__checkInt(ev.event.range.max, "Non-int random mouse move range max");
__checkRangeMinMax(ev.event.range, "Invalid random mouse move range");
} else {
throw `Unknown event type: ${event.event_type}`;
throw `Unknown event type: ${ev.event_type}`;
}
events.push(event);
events.push(ev);
}
__events = events;
@@ -274,26 +277,29 @@ export function Recorder() {
var __runEvents = function(index, time=0) {
while (index < __events.length) {
__setCounters(__events.length - index + 1, __events_time - time);
let event = __events[index];
let ev = __events[index];
if (["delay", "delay_random"].includes(event.event_type)) {
if (["delay", "delay_random"].includes(ev.event_type)) {
let millis = (
event.event_type === "delay"
? event.event.millis
: tools.getRandomInt(event.event.range.min, event.event.range.max)
ev.event_type === "delay"
? ev.event.millis
: tools.getRandomInt(ev.event.range.min, ev.event.range.max)
);
__play_timer = setTimeout(() => __runEvents(index + 1, time + millis), millis);
return;
} else if (event.event_type === "print") {
} else if (ev.event_type === "print") {
let params = {"limit": 0};
if (event.event.keymap !== undefined) {
params["keymap"] = event.event.keymap;
if (ev.event.keymap !== undefined) {
params["keymap"] = ev.event.keymap;
}
if (event.event.slow !== undefined) {
params["slow"] = event.event.slow;
if (ev.event.slow !== undefined) {
params["slow"] = ev.event.slow;
}
tools.httpPost("/api/hid/print", params, function(http) {
if (ev.event.delay !== undefined) {
params["delay"] = ev.event.delay / 1000;
}
tools.httpPost("api/hid/print", params, function(http) {
if (http.status === 413) {
wm.error("Too many text for paste!");
__stopProcess();
@@ -303,11 +309,11 @@ export function Recorder() {
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
}
}, event.event.text, "text/plain");
}, ev.event.text, "text/plain");
return;
} else if (event.event_type === "atx_button") {
tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) {
} else if (ev.event_type === "atx_button") {
tools.httpPost("api/atx/click", {"button": ev.event.button}, function(http) {
if (http.status !== 200) {
wm.error("ATX error", http.responseText);
__stopProcess();
@@ -317,12 +323,12 @@ export function Recorder() {
});
return;
} else if (["gpio_switch", "gpio_pulse"].includes(event.event_type)) {
let path = "/api/gpio";
let params = {"channel": event.event.channel};
if (event.event_type === "gpio_switch") {
} else if (["gpio_switch", "gpio_pulse"].includes(ev.event_type)) {
let path = "api/gpio";
let params = {"channel": ev.event.channel};
if (ev.event_type === "gpio_switch") {
path += "/switch";
params["state"] = event.event.to;
params["state"] = ev.event.to;
} else { // gpio_pulse
path += "/pulse";
}
@@ -336,19 +342,19 @@ export function Recorder() {
});
return;
} else if (event.event_type === "key") {
event.event.finish = $("hid-keyboard-bad-link-switch").checked;
__ws.sendHidEvent(event);
} else if (ev.event_type === "key") {
ev.event.finish = $("hid-keyboard-bad-link-switch").checked;
__ws.sendHidEvent(ev);
} else if (["mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
__ws.sendHidEvent(event);
} else if (["mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(ev.event_type)) {
__ws.sendHidEvent(ev);
} else if (event.event_type === "mouse_move_random") {
} else if (ev.event_type === "mouse_move_random") {
__ws.sendHidEvent({
"event_type": "mouse_move",
"event": {"to": {
"x": tools.getRandomInt(event.event.range.min, event.event.range.max),
"y": tools.getRandomInt(event.event.range.min, event.event.range.max),
"x": tools.getRandomInt(ev.event.range.min, ev.event.range.max),
"y": tools.getRandomInt(ev.event.range.min, ev.event.range.max),
}},
});
}

View File

@@ -27,6 +27,7 @@
import {tools, $} from "../tools.js";
import {wm} from "../wm.js";
import {Info} from "./info.js";
import {Recorder} from "./recorder.js";
import {Hid} from "./hid.js";
import {Paste} from "./paste.js";
@@ -48,6 +49,7 @@ export function Session() {
var __ping_timer = null;
var __missed_heartbeats = 0;
var __info = new Info();
var __streamer = new Streamer();
var __recorder = new Recorder();
var __hid = new Hid(__streamer.getGeometry, __recorder);
@@ -58,252 +60,36 @@ export function Session() {
var __ocr = new Ocr(__streamer.getGeometry);
var __switch = new Switch();
var __info_hw_state = null;
var __info_fan_state = null;
var __init__ = function() {
__streamer.ensureDeps(() => __startSession());
};
/************************************************************************/
var __setInfoState = function(state) {
for (let key of Object.keys(state)) {
switch (key) {
case "meta": __setInfoStateMeta(state.meta); break;
case "hw": __setInfoStateHw(state.hw); break;
case "fan": __setInfoStateFan(state.fan); break;
case "system": __setInfoStateSystem(state.system); break;
case "extras": __setInfoStateExtras(state.extras); break;
}
}
};
var __setInfoStateMeta = function(state) {
if (state !== null) {
$("kvmd-meta-json").innerText = JSON.stringify(state, undefined, 4);
if (state.server && state.server.host) {
$("kvmd-meta-server-host").innerText = `Server: ${state.server.host}`;
document.title = `One-KVM Session: ${state.server.host}`;
} else {
$("kvmd-meta-server-host").innerText = "";
document.title = "One-KVM Session";
}
if (state.tips && state.tips.left) {
$("kvmd-meta-tips-left").innerText = `${state.tips.left}`;
}
if (state.tips && state.tips.right) {
$("kvmd-meta-tips-right").innerText = `${state.tips.right}`;
}
// Don't use this option, it may be removed in any time
if (state.web && state.web.confirm_session_exit === false) {
window.onbeforeunload = null; // See main.js
}
}
};
var __setInfoStateHw = function(state) {
if (state.health.throttling !== null) {
let flags = state.health.throttling.parsed_flags;
let ignore_past = state.health.throttling.ignore_past;
let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past));
let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past));
tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped));
$("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden");
$("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden");
tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage);
tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped);
}
__info_hw_state = state;
__renderAboutInfoHardware();
};
var __setInfoStateFan = function(state) {
let failed = false;
let failed_past = false;
if (state.monitored) {
if (state.state === null) {
failed = true;
} else {
if (!state.state.fan.ok) {
failed = true;
} else if (state.state.fan.last_fail_ts >= 0) {
failed = true;
failed_past = true;
}
}
}
tools.hidden.setVisible($("fan-health-dropdown"), failed);
$("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden");
__info_fan_state = state;
__renderAboutInfoHardware();
};
var __renderAboutInfoHardware = function() {
let html = "";
if (__info_hw_state !== null) {
html += `
Platform:
${__formatMisc(__info_hw_state)}
<hr>
Temperature:
${__formatTemp(__info_hw_state.health.temp)}
<hr>
Throttling:
${__formatThrottling(__info_hw_state.health.throttling)}
`;
}
if (__info_fan_state !== null) {
if (html.length > 0) {
html += "<hr>";
}
html += `
Fan:
${__formatFan(__info_fan_state)}
`;
}
$("about-hardware").innerHTML = html;
};
var __formatMisc = function(state) {
return __formatUl([
["Base", state.platform.base],
["Serial", state.platform.serial],
["CPU", `${state.health.cpu.percent}%`],
["MEM", `${state.health.mem.percent}%`],
]);
};
var __formatFan = function(state) {
if (!state.monitored) {
return __formatUl([["Status", "Not monitored"]]);
} else if (state.state === null) {
return __formatUl([["Status", __colored("red", "Not available")]]);
} else {
state = state.state;
let pairs = [
["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))],
["Desired speed", `${state.fan.speed}%`],
["PWM", `${state.fan.pwm}`],
];
if (state.hall.available) {
pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]);
}
return __formatUl(pairs);
}
};
var __formatTemp = function(temp) {
let pairs = [];
for (let field of Object.keys(temp).sort()) {
pairs.push([field.toUpperCase(), `${temp[field]}&deg;C`]);
}
return __formatUl(pairs);
};
var __formatThrottling = function(throttling) {
if (throttling !== null) {
let pairs = [];
for (let field of Object.keys(throttling.parsed_flags).sort()) {
let flags = throttling.parsed_flags[field];
let key = tools.upperFirst(field).replace("_", " ");
let value = (flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No"));
if (!throttling.ignore_past) {
value += "; " + (flags["past"] ? __colored("red", "In the past") : __colored("green", "Never"));
}
pairs.push([key, value]);
}
return __formatUl(pairs);
} else {
return "NO DATA";
}
};
var __colored = function(color, html) {
return `<font color="${color}">${html}</font>`;
};
var __setInfoStateSystem = function(state) {
$("about-version").innerHTML = `
KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
<hr>
Streamer: <span class="code-comment">${state.streamer.version} (${state.streamer.app})</span>
${__formatStreamerFeatures(state.streamer.features)}
<hr>
${state.kernel.system} kernel:
${__formatUname(state.kernel)}
`;
$("kvmd-version-kvmd").innerText = state.kvmd.version;
$("kvmd-version-streamer").innerText = state.streamer.version;
};
var __formatStreamerFeatures = function(features) {
let pairs = [];
for (let field of Object.keys(features).sort()) {
pairs.push([field, (features[field] ? "Yes" : "No")]);
}
return __formatUl(pairs);
};
var __formatUname = function(kernel) {
let pairs = [];
for (let field of Object.keys(kernel).sort()) {
if (field !== "system") {
pairs.push([tools.upperFirst(field), kernel[field]]);
}
}
return __formatUl(pairs);
};
var __formatUl = function(pairs) {
let html = "";
for (let pair of pairs) {
html += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`;
}
return `<ul>${html}</ul>`;
};
var __setInfoStateExtras = function(state) {
let show_hook = null;
let close_hook = null;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
if (has_webterm) {
let path = "/" + state.webterm.path + "?disableLeaveAlert=true";
show_hook = function() {
tools.info("Terminal opened: ", path);
$("webterm-iframe").src = path;
};
close_hook = function() {
tools.info("Terminal closed");
$("webterm-iframe").src = "";
};
}
tools.feature.setEnabled($("system-tool-webterm"), has_webterm);
$("webterm-window").show_hook = show_hook;
$("webterm-window").close_hook = close_hook;
};
var __startSession = function() {
$("link-led").className = "led-yellow";
$("link-led").title = "Connecting...";
tools.httpGet("/api/auth/check", null, function(http) {
tools.httpGet("api/auth/check", null, function(http) {
if (http.status === 200) {
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
__ws = new WebSocket(tools.makeWsUrl("api/ws"));
__ws.sendHidEvent = (ev) => __sendHidEvent(__ws, ev.event_type, ev.event);
__ws.binaryType = "arraybuffer";
__ws.onopen = __wsOpenHandler;
__ws.onmessage = __wsMessageHandler;
__ws.onmessage = async (ev) => {
if (typeof ev.data === "string") {
ev = JSON.parse(ev.data);
__wsJsonHandler(ev.event_type, ev.event);
} else { // Binary
__wsBinHandler(ev.data);
}
};
__ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler;
} else if (http.status === 401 || http.status === 403) {
window.onbeforeunload = () => null;
wm.error("Unexpected logout occured, please login again").then(function() {
document.location.href = "/login";
tools.currentOpen("login");
});
} else {
__wsCloseHandler(null);
@@ -311,51 +97,8 @@ export function Session() {
});
};
var __ascii_encoder = new TextEncoder("ascii");
var __sendHidEvent = function(ws, event_type, event) {
if (event_type == "key") {
let data = __ascii_encoder.encode("\x01\x00" + event.key);
data[1] = (event.state ? 1 : 0);
if (event.finish === true) { // Optional
data[1] |= 0x02;
}
ws.send(data);
} else if (event_type == "mouse_button") {
let data = __ascii_encoder.encode("\x02\x00" + event.button);
data[1] = (event.state ? 1 : 0);
ws.send(data);
} else if (event_type == "mouse_move") {
let data = new Uint8Array([
3,
(event.to.x >> 8) & 0xFF, event.to.x & 0xFF,
(event.to.y >> 8) & 0xFF, event.to.y & 0xFF,
]);
ws.send(data);
} else if (event_type == "mouse_relative" || event_type == "mouse_wheel") {
let data;
if (Array.isArray(event.delta)) {
data = new Int8Array(2 + event.delta.length * 2);
let index = 0;
for (let delta of event.delta) {
data[index + 2] = delta["x"];
data[index + 3] = delta["y"];
index += 2;
}
} else {
data = new Int8Array([0, 0, event.delta.x, event.delta.y]);
}
data[0] = (event_type == "mouse_relative" ? 4 : 5);
data[1] = (event.squash ? 1 : 0);
ws.send(data);
}
};
var __wsOpenHandler = function(event) {
tools.debug("Session: socket opened:", event);
var __wsOpenHandler = function(ev) {
tools.debug("Session: socket opened:", ev);
$("link-led").className = "led-green";
$("link-led").title = "Connected";
__recorder.setSocket(__ws);
@@ -364,39 +107,43 @@ export function Session() {
__ping_timer = setInterval(__pingServer, 1000);
};
var __wsMessageHandler = function(event) {
// tools.debug("Session: received socket data:", event.data);
let data = JSON.parse(event.data);
switch (data.event_type) {
case "pong": __missed_heartbeats = 0; break;
case "info": __setInfoState(data.event); break;
case "gpio": __gpio.setState(data.event); break;
case "hid": __hid.setState(data.event); break;
case "hid_keymaps": __paste.setState(data.event); break;
case "atx": __atx.setState(data.event); break;
case "streamer": __streamer.setState(data.event); break;
case "ocr": __ocr.setState(data.event); break;
var __wsBinHandler = function(data) {
data = new Uint8Array(data);
if (data[0] === 255) { // Pong
__missed_heartbeats = 0;
}
};
var __wsJsonHandler = function(ev_type, ev) {
switch (ev_type) {
case "info": __info.setState(ev); break;
case "gpio": __gpio.setState(ev); break;
case "hid": __hid.setState(ev); break;
case "hid_keymaps": __paste.setState(ev); break;
case "atx": __atx.setState(ev); break;
case "streamer": __streamer.setState(ev); break;
case "ocr": __ocr.setState(ev); break;
case "msd":
if (data.event.online === false) {
if (ev.online === false) {
__switch.setMsdConnected(false);
} else if (data.event.drive !== undefined) {
__switch.setMsdConnected(data.event.drive.connected);
} else if (ev.drive !== undefined) {
__switch.setMsdConnected(ev.drive.connected);
}
__msd.setState(data.event);
__msd.setState(ev);
break;
case "switch":
if (data.event.model) {
__atx.setHasSwitch(data.event.model.ports.length > 0);
if (ev.model) {
__atx.setHasSwitch(ev.model.ports.length > 0);
}
__switch.setState(data.event);
__switch.setState(ev);
break;
}
};
var __wsErrorHandler = function(event) {
tools.error("Session: socket error:", event);
var __wsErrorHandler = function(ev) {
tools.error("Session: socket error:", ev);
if (__ws) {
__ws.onclose = null;
__ws.close();
@@ -404,9 +151,8 @@ export function Session() {
}
};
var __wsCloseHandler = function(event) {
tools.debug("Session: socket closed:", event);
var __wsCloseHandler = function(ev) {
tools.debug("Session: socket closed:", ev);
$("link-led").className = "led-gray";
if (__ping_timer) {
@@ -437,11 +183,54 @@ export function Session() {
if (__missed_heartbeats >= 15) {
throw new Error("Too many missed heartbeats");
}
__ws.send("{\"event_type\": \"ping\", \"event\": {}}");
__ws.send(new Uint8Array([0]));
} catch (ex) {
__wsErrorHandler(ex.message);
}
};
var __ascii_encoder = new TextEncoder("ascii");
var __sendHidEvent = function(ws, ev_type, ev) {
if (ev_type === "key") {
let data = __ascii_encoder.encode("\x01\x00" + ev.key);
data[1] = (ev.state ? 1 : 0);
if (ev.finish === true) { // Optional
data[1] |= 0x02;
}
ws.send(data);
} else if (ev_type === "mouse_button") {
let data = __ascii_encoder.encode("\x02\x00" + ev.button);
data[1] = (ev.state ? 1 : 0);
ws.send(data);
} else if (ev_type === "mouse_move") {
let data = new Uint8Array([
3,
(ev.to.x >> 8) & 0xFF, ev.to.x & 0xFF,
(ev.to.y >> 8) & 0xFF, ev.to.y & 0xFF,
]);
ws.send(data);
} else if (ev_type === "mouse_relative" || ev_type === "mouse_wheel") {
let data;
if (Array.isArray(ev.delta)) {
data = new Int8Array(2 + ev.delta.length * 2);
let index = 0;
for (let delta of ev.delta) {
data[index + 2] = delta["x"];
data[index + 3] = delta["y"];
index += 2;
}
} else {
data = new Int8Array([0, 0, ev.delta.x, ev.delta.y]);
}
data[0] = (ev_type === "mouse_relative" ? 4 : 5);
data[1] = (ev.squash ? 1 : 0);
ws.send(data);
}
};
__init__();
}

View File

@@ -44,9 +44,9 @@ export function Streamer() {
var __res = {"width": 640, "height": 480};
var __init__ = function() {
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo, __organizeHook);
$("stream-led").title = "Stream inactive";
$("stream-led").title = "No stream from PiKVM";
tools.slider.setParams($("stream-quality-slider"), 5, 100, 5, 80, function(value) {
$("stream-quality-value").innerText = `${value}%`;
@@ -73,13 +73,13 @@ export function Streamer() {
tools.radio.setOnClick("stream-mode-radio", __clickModeRadio, false);
// Not getInt() because of radio is a string container.
// Also don't reset Janus at class init.
// Also don't reset Streamer at class init.
tools.radio.clickValue("stream-orient-radio", tools.storage.get("stream.orient", 0));
tools.radio.setOnClick("stream-orient-radio", function() {
if (__streamer.getMode() === "janus") { // Right now it's working only for H.264
if (["janus", "media"].includes(__streamer.getMode())) {
let orient = parseInt(tools.radio.getValue("stream-orient-radio"));
tools.storage.setInt("stream.orient", orient);
if (__streamer.getOrientation() != orient) {
if (__streamer.getOrientation() !== orient) {
__resetStream();
}
}
@@ -112,15 +112,37 @@ export function Streamer() {
tools.el.setOnClick($("stream-record-stop-button"), __clickRecordStopButton);
$("stream-window").show_hook = () => __applyState(__state);
$("stream-window").close_hook = () => __applyState(null);
tools.storage.bindSimpleSwitch($("stream-suspend-switch"), "stream.suspend", false, __visibilityHook);
//hidden stream-record-stop-button
document.getElementById('stream-record-stop-button').disabled = true;
$("stream-window").show_hook = __visibilityHook;
$("stream-window").close_hook = __visibilityHook;
$("stream-window").organize_hook = __organizeHook;
document.addEventListener("visibilitychange", __visibilityHook);
};
/************************************************************************/
var __isStreamRequired = function() {
return (
wm.isWindowVisible($("stream-window"))
&& (
!$("stream-suspend-switch").checked
|| (document.visibilityState === "visible")
)
);
};
var __visibilityHook = function() {
let req = __isStreamRequired();
__applyState(req ? __state : null);
};
var __organizeHook = function() {
let geo = self.getGeometry();
wm.setAspectRatio($("stream-window"), geo.width, geo.height);
};
self.ensureDeps = function(callback) {
JanusStreamer.ensure_janus(function(avail) {
__janus_imported = avail;
@@ -167,13 +189,13 @@ export function Streamer() {
__state = null;
__setControlsEnabled(false);
}
let visible = wm.isWindowVisible($("stream-window"));
__applyState((visible && __state && __state.features) ? state : null);
__applyState((__isStreamRequired() && __state && __state.features) ? state : null);
};
var __applyState = function(state) {
if (__janus_imported === null) {
alert("__janus_imported is null, please report");
// XXX: This warning is triggered by visibilitychange event via the __visibilityHook()
// alert("__janus_imported is null, please report");
return;
}
@@ -267,7 +289,7 @@ export function Streamer() {
var __setInactive = function() {
$("stream-led").className = "led-gray";
$("stream-led").title = "Stream inactive";
$("stream-led").title = "No stream from PiKVM";
};
var __setControlsEnabled = function(enabled) {
@@ -285,7 +307,7 @@ export function Streamer() {
let title = `${__streamer.getName()} - `;
if (is_active) {
if (!online) {
title += "No signal / ";
title += "No video from host / ";
}
title += `${__res.width}x${__res.height}`;
if (text.length > 0) {
@@ -295,7 +317,7 @@ export function Streamer() {
if (text.length > 0) {
title += text;
} else {
title += "Inactive";
title += "No stream from PiKVM";
}
}
el_grab.innerText = el_info.innerText = title;
@@ -306,27 +328,26 @@ export function Streamer() {
mode = __streamer.getMode();
}
__streamer.stopStream();
if (mode === "mjpeg") {
// For mjpeg mode, create an instance of MjpegStreamer
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
tools.feature.setEnabled($("stream-orient"), false);
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
tools.feature.setEnabled($("stream-mic"), false); // Ditto
} else if (mode === "media") {
// For media mode, create an instance of MediaStreamer
__streamer = new MediaStreamer(__setActive, __setInactive, __setInfo);
tools.feature.setEnabled($("stream-orient"), false);
tools.feature.setEnabled($("stream-audio"), false); // Assuming this should be disabled for MediaStreamer as well
tools.feature.setEnabled($("stream-mic"), false); // Ditto
} else { // janus
// For janus mode, create an instance of JanusStreamer with specific settings
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo,
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted, $("stream-mic-switch").checked);
let orient = tools.storage.getInt("stream.orient", 0);
if (mode === "janus") {
let allow_audio = !$("stream-video").muted;
let allow_mic = $("stream-mic-switch").checked;
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo, __organizeHook, orient, allow_audio, allow_mic);
// Firefox doesn't support RTP orientation:
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1316448
tools.feature.setEnabled($("stream-orient"), !tools.browser.is_firefox);
} else {
if (mode === "media") {
__streamer = new MediaStreamer(__setActive, __setInactive, __setInfo, __organizeHook, orient);
tools.feature.setEnabled($("stream-orient"), true);
} else { // mjpeg
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo, __organizeHook);
tools.feature.setEnabled($("stream-orient"), false);
}
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
tools.feature.setEnabled($("stream-mic"), false); // Ditto
}
if (wm.isWindowVisible($("stream-window"))) {
if (__isStreamRequired()) {
__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
}
};
@@ -343,19 +364,14 @@ export function Streamer() {
};
var __clickScreenshotButton = function() {
let el = document.createElement("a");
el.href = "/api/streamer/snapshot";
el.target = "_blank";
document.body.appendChild(el);
el.click();
setTimeout(() => document.body.removeChild(el), 0);
tools.windowOpen("api/streamer/snapshot");
};
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset stream?").then(function(ok) {
wm.confirm("Are you sure you want to reset the stream?").then(function(ok) {
if (ok) {
__resetStream();
tools.httpPost("/api/streamer/reset", null, function(http) {
tools.httpPost("api/streamer/reset", null, function(http) {
if (http.status !== 200) {
wm.error("Can't reset stream", http.responseText);
}
@@ -440,7 +456,7 @@ export function Streamer() {
};
var __sendParam = function(name, value) {
tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) {
tools.httpPost("api/streamer/set_params", {[name]: value}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure stream", http.responseText);
}

View File

@@ -30,7 +30,7 @@ import {tools, $} from "../tools.js";
var _Janus = null;
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio, __allow_mic) {
export function JanusStreamer(__setActive, __setInactive, __setInfo, __organizeHook, __orient, __allow_audio, __allow_mic) {
var self = this;
/************************************************************************/
@@ -49,6 +49,10 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
var __state = null;
var __frames = 0;
var __res = {"width": -1, "height": -1};
var __resize_listener_installed = false;
var __ice = null;
/************************************************************************/
@@ -83,11 +87,28 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
__state = state;
__stop = false;
__ensureJanus(false);
if (!__resize_listener_installed) {
$("stream-video").addEventListener("resize", __videoResizeHandler);
__resize_listener_installed = true;
}
};
self.stopStream = function() {
__stop = true;
__destroyJanus();
if (__resize_listener_installed) {
$("stream-video").removeEventListener("resize", __videoResizeHandler);
__resize_listener_installed = false;
}
};
var __videoResizeHandler = function(ev) {
let el = ev.target;
if (__res.width !== el.videoWidth || __res.height !== el.videoHeight) {
__res.width = el.videoWidth;
__res.height = el.videoHeight;
__organizeHook();
}
};
var __ensureJanus = function(internal) {
@@ -97,9 +118,10 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
__setInfo(false, false, "");
__logInfo("Starting Janus ...");
__janus = new _Janus({
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
"server": tools.makeWsUrl("janus/ws"),
"ipv6": true,
"destroyOnUnload": false,
"iceServers": () => __getIceServers(),
"success": __attachJanus,
"error": function(error) {
__logError(error);
@@ -110,6 +132,15 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
}
};
var __getIceServers = function() {
if (__ice !== null && __ice.url) {
__logInfo("Using the custom ICE Server got from uStreamer:", __ice);
return [{"urls": __ice.url}];
} else {
return [];
}
};
var __finishJanus = function() {
if (__stop) {
if (__retry_ensure_timeout !== null) {
@@ -202,7 +233,8 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
"success": function(handle) {
__handle = handle;
__logInfo("uStreamer attached:", handle.getPlugin(), handle.getId());
__sendWatch();
__logInfo("Sending FEATURES ...");
__handle.send({"message": {"request": "features"}});
},
"error": function(error) {
@@ -233,7 +265,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
__stopRetryEmsgInterval();
if (msg.result) {
__logInfo("Got uStreamer result message:", msg.result.status); // starting, started, stopped
__logInfo("Got uStreamer result message:", msg.result); // starting, started, stopped
if (msg.result.status === "started") {
__setActive();
__setInfo(false, false, "");
@@ -243,6 +275,8 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
} else if (msg.result.status === "features") {
tools.feature.setEnabled($("stream-audio"), msg.result.features.audio);
tools.feature.setEnabled($("stream-mic"), msg.result.features.mic);
__ice = msg.result.features.ice;
__sendWatch();
}
} else if (msg.error_code || msg.error) {
__logError("Got uStreamer error message:", msg.error_code, "-", msg.error);
@@ -298,7 +332,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
// Chrome sends `muted` notifiation for tracks in `disconnected` ICE state
// and Janus.js just removes muted track from list of available tracks.
// But track still exists actually so it's safe to just ignore
// reason == "mute" and "unmute".
// reason === "mute" and "unmute".
let reason = (meta || {}).reason;
__logInfo("Got onremotetrack:", id, added, reason, track, meta);
if (added && reason === "created") {
@@ -412,8 +446,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
var __sendWatch = function() {
if (__handle) {
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}, mic=${__allow_mic}) + FEATURES ...`);
__handle.send({"message": {"request": "features"}});
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}, mic=${__allow_mic}) ...`);
__handle.send({"message": {"request": "watch", "params": {
"orientation": __orient,
"audio": __allow_audio,

View File

@@ -26,7 +26,7 @@
import {tools, $} from "../tools.js";
export function MediaStreamer(__setActive, __setInactive, __setInfo) {
export function MediaStreamer(__setActive, __setInactive, __setInfo, __organizeHook, __orient) {
var self = this;
/************************************************************************/
@@ -37,17 +37,20 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
var __ws = null;
var __ping_timer = null;
var __missed_heartbeats = 0;
var __decoder = null;
var __codec = "";
var __decoder = null;
var __frame = null;
var __canvas = $("stream-canvas");
var __ctx = __canvas.getContext("2d");
var __state = null;
var __frames = 0;
var __fps_accum = 0;
/************************************************************************/
self.getName = () => "HTTP H.264";
self.getOrientation = () => __orient;
self.getName = () => "Direct H.264";
self.getMode = () => "media";
self.getResolution = function() {
@@ -79,23 +82,28 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
__setInactive();
__setInfo(false, false, "");
__logInfo("Starting Media ...");
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/media/ws`);
__ws = new WebSocket(tools.makeWsUrl("api/media/ws"));
__ws.binaryType = "arraybuffer";
__ws.onopen = __wsOpenHandler;
__ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler;
__ws.onmessage = async (event) => {
if (typeof event.data === "string") {
__wsJsonHandler(JSON.parse(event.data));
} else { // Binary
await __wsBinHandler(event.data);
__ws.onmessage = async (ev) => {
try {
if (typeof ev.data === "string") {
ev = JSON.parse(ev.data);
__wsJsonHandler(ev.event_type, ev.event);
} else { // Binary
await __wsBinHandler(ev.data);
}
} catch (ex) {
__wsErrorHandler(ex);
}
};
}
};
var __wsOpenHandler = function(event) {
__logInfo("Socket opened:", event);
var __wsOpenHandler = function(ev) {
__logInfo("Socket opened:", ev);
__missed_heartbeats = 0;
__ping_timer = setInterval(__ping, 1000);
};
@@ -110,8 +118,8 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
if (__decoder && __decoder.state === "configured") {
let online = !!(__state && __state.source.online);
let info = `${__frames} fps dynamic`;
__frames = 0;
let info = `${__fps_accum} fps dynamic`;
__fps_accum = 0;
__setInfo(true, online, info);
}
} catch (ex) {
@@ -128,64 +136,35 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
__setInactive();
};
var __wsErrorHandler = function(event) {
__logInfo("Socket error:", event);
__setInfo(false, false, event);
var __wsErrorHandler = function(ev) {
__logInfo("Socket error:", ev);
__setInfo(false, false, ev);
__wsForceClose();
};
var __wsCloseHandler = function(event) {
__logInfo("Socket closed:", event);
var __wsCloseHandler = function(ev) {
__logInfo("Socket closed:", ev);
if (__ping_timer) {
clearInterval(__ping_timer);
__ping_timer = null;
}
if (__decoder) {
__decoder.close();
__decoder = null;
}
__closeDecoder();
__missed_heartbeats = 0;
__frames = 0;
__fps_accum = 0;
__ws = null;
if (!__stop) {
setTimeout(() => __ensureMedia(true), 1000);
}
};
var __wsJsonHandler = function(event) {
if (event.event_type === "media") {
__decoderCreate(event.event.video);
var __wsJsonHandler = function(ev_type, ev) {
if (ev_type === "media") {
__setupCodec(ev.video);
}
};
var __wsBinHandler = async (data) => {
let header = new Uint8Array(data.slice(0, 2));
if (header[0] === 255) { // Pong
__missed_heartbeats = 0;
} else if (header[0] === 1 && __decoder !== null) { // Video frame
let key = !!header[1];
if (__decoder.state !== "configured") {
if (!key) {
return;
}
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
__setActive();
}
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
"type": (key ? "key" : "delta"),
"data": data.slice(2),
});
await __decoder.decode(chunk);
}
};
var __decoderCreate = function(formats) {
__decoderDestroy();
var __setupCodec = function(formats) {
__closeDecoder();
if (formats.h264 === undefined) {
let msg = "No H.264 stream available on PiKVM";
__setInfo(false, false, msg);
@@ -201,35 +180,144 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
__logInfo(msg);
return;
}
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
"output": (frame) => {
try {
if (__canvas.width !== frame.displayWidth || __canvas.height !== frame.displayHeight) {
__canvas.width = frame.displayWidth;
__canvas.height = frame.displayHeight;
}
__ctx.drawImage(frame, 0, 0);
__frames += 1;
} finally {
frame.close();
}
},
"error": (err) => __logInfo(err.message),
});
__codec = `avc1.${formats.h264.profile_level_id}`;
__ws.send(JSON.stringify({
"event_type": "start",
"event": {"type": "video", "format": "h264"},
}));
};
var __decoderDestroy = function() {
var __wsBinHandler = async (data) => {
let header = new Uint8Array(data.slice(0, 2));
if (header[0] === 255) { // Pong
__missed_heartbeats = 0;
} else if (header[0] === 1) { // Video frame
let key = !!header[1];
if (await __ensureDecoder(key)) {
await __processFrame(key, data.slice(2));
}
}
};
var __ensureDecoder = async (key) => {
if (__codec === "") {
return false;
}
if (__decoder === null || __decoder.state === "closed") {
let started = (__codec !== "");
let codec = __codec;
__closeDecoder();
__codec = codec;
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
"output": __renderFrame,
"error": (err) => __logInfo(err.message),
});
if (started) {
__ws.send(new Uint8Array([0]));
}
}
if (__decoder.state !== "configured") {
if (!key) {
return false;
}
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
}
if (__decoder.state === "configured") {
__setActive();
return true;
}
return false;
};
var __processFrame = async (key, raw) => {
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
"type": (key ? "key" : "delta"),
"data": raw,
});
await __decoder.decode(chunk);
};
var __closeDecoder = function() {
if (__decoder !== null) {
__decoder.close();
__decoder = null;
__codec = "";
try {
__decoder.close();
} finally {
__codec = "";
__decoder = null;
if (__frame !== null) {
try {
__closeFrame(__frame);
} finally {
__frame = null;
}
}
}
}
};
var __renderFrame = function(frame) {
if (__frame === null) {
__frame = frame;
window.requestAnimationFrame(__drawPendingFrame, __canvas);
} else {
__closeFrame(frame);
}
};
var __drawPendingFrame = function() {
if (__frame === null) {
return;
}
try {
let width = __frame.displayWidth;
let height = __frame.displayHeight;
switch (__orient) {
case 90:
case 270:
width = __frame.displayHeight;
height = __frame.displayWidth;
}
if (__canvas.width !== width || __canvas.height !== height) {
__canvas.width = width;
__canvas.height = height;
__organizeHook();
}
if (__orient === 0) {
__ctx.drawImage(__frame, 0, 0);
} else {
__ctx.save();
try {
switch(__orient) {
case 90: __ctx.translate(0, height); __ctx.rotate(-Math.PI / 2); break;
case 180: __ctx.translate(width, height); __ctx.rotate(-Math.PI); break;
case 270: __ctx.translate(width, 0); __ctx.rotate(Math.PI / 2); break;
}
__ctx.drawImage(__frame, 0, 0);
} finally {
__ctx.restore();
}
}
__fps_accum += 1;
} finally {
__closeFrame(__frame);
__frame = null;
}
};
var __closeFrame = function(frame) {
if (!tools.browser.is_firefox) {
// FIXME: On Firefox, image is flickering when we're closing the frame for some reason.
// So we're just not performing the close() and it seems there is no problems here
// because Firefox is implementing some auto-closing logic. With auto-close,
// no flickering observed.
// - https://github.com/mozilla/gecko-dev/blob/82333a9/dom/media/webcodecs/VideoFrame.cpp
// Note at 2025.05.13:
// - The problem is not observed on nightly Firefox 140.
// - It's also not observed with hardware accelleration on 138.
frame.close();
}
};

View File

@@ -23,15 +23,16 @@
"use strict";
import {ROOT_PREFIX} from "../vars.js";
import {tools, $} from "../tools.js";
export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
export function MjpegStreamer(__setActive, __setInactive, __setInfo, __organizeHook) {
var self = this;
/************************************************************************/
var __key = tools.makeId();
var __key = tools.makeRandomId();
var __id = "";
var __fps = -1;
var __state = null;
@@ -61,6 +62,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
if (__id.length > 0 && __id in __state.stream.clients_stat) {
__setStreamActive();
__stopChecking();
__organizeHook();
} else {
__ensureChecking();
}
@@ -72,7 +74,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
self.stopStream = function() {
self.ensureStream(null);
let blank = "/share/png/blank-stream.png";
let blank = `${ROOT_PREFIX}share/png/blank-stream.png`;
if (!String.prototype.endsWith.call($("stream-image").src, blank)) {
$("stream-image").src = blank;
}
@@ -90,7 +92,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
var __setStreamInactive = function() {
let old_fps = __fps;
__key = tools.makeId();
__key = tools.makeRandomId();
__id = "";
__fps = -1;
__state = null;
@@ -127,7 +129,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
var __checkStream = function() {
__findId();
if (__id.legnth > 0 && __id in __state.stream.clients_stat) {
if (__id.length > 0 && __id in __state.stream.clients_stat) {
__setStreamActive();
__stopChecking();
@@ -138,7 +140,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
__setStreamInactive();
__stopChecking();
let path = `/streamer/stream?key=${__key}`;
let path = `${ROOT_PREFIX}streamer/stream?key=${encodeURIComponent(__key)}`;
if (tools.browser.is_safari || tools.browser.is_ios) {
// uStreamer fix for WebKit
__logInfo("Using dual_final_frames=1 to fix WebKit bugs");

View File

@@ -23,8 +23,10 @@
"use strict";
import {ROOT_PREFIX} from "../vars.js";
import {tools, $} from "../tools.js";
import {wm} from "../wm.js";
import {clipboard} from "./clipboard.js";
export function Switch() {
@@ -44,6 +46,7 @@ export function Switch() {
tools.el.setOnClick($("switch-edid-copy-data-button"), __clickCopyEdidDataButton);
tools.storage.bindSimpleSwitch($("switch-atx-ask-switch"), "switch.atx.ask", true);
tools.storage.bindSimpleSwitch($("switch-msd-ask-switch"), "switch.msd.ask", true);
for (let role of ["inactive", "active", "flashing", "beacon", "bootloader"]) {
let el_brightness = $(`switch-color-${role}-brightness-slider`);
@@ -125,7 +128,7 @@ export function Switch() {
+ ":" + brightness.toString(16).padStart(2, "0")
+ ":" + color.blink_ms.toString(16).padStart(4, "0")
);
__sendPost("/api/switch/set_colors", {[role]: rgbx}, function() {
__sendPost("api/switch/set_colors", {[role]: rgbx}, function() {
el_color.value = (
"#"
+ color.red.toString(16).padStart(2, "0")
@@ -137,7 +140,7 @@ export function Switch() {
};
var __clickSetDefaultColorButton = function(role) {
__sendPost("/api/switch/set_colors", {[role]: "default"});
__sendPost("api/switch/set_colors", {[role]: "default"});
};
var __applyEdids = function(edids) {
@@ -177,8 +180,8 @@ export function Switch() {
};
var __clickAddEdidButton = function() {
let create_content = function(el_parent, el_ok_button) {
tools.el.setEnabled(el_ok_button, false);
let create_content = function(el_parent, el_ok_bt) {
tools.el.setEnabled(el_ok_bt, false);
el_parent.innerHTML = `
<table>
<tr>
@@ -202,7 +205,7 @@ export function Switch() {
el_name.oninput = el_data.oninput = function() {
let name = el_name.value.replace(/\s+/g, "");
let data = el_data.value.replace(/\s+/g, "");
tools.el.setEnabled(el_ok_button, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data)));
tools.el.setEnabled(el_ok_bt, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data)));
};
};
@@ -210,7 +213,7 @@ export function Switch() {
if (ok) {
let name = $("__switch-edid-new-name-input").value;
let data = $("__switch-edid-new-data-text").value;
__sendPost("/api/switch/edids/create", {"name": name, "data": data});
__sendPost("api/switch/edids/create", {"name": name, "data": data});
}
});
};
@@ -222,7 +225,7 @@ export function Switch() {
let html = "Are you sure to remove this EDID?<br>Ports that used it will change it to the default.";
wm.confirm(html, name).then(function(ok) {
if (ok) {
__sendPost("/api/switch/edids/remove", {"id": edid_id});
__sendPost("api/switch/edids/remove", {"id": edid_id});
}
});
}
@@ -233,7 +236,7 @@ export function Switch() {
if (edid_id && __state && __state.edids) {
let data = __state.edids.all[edid_id].data;
data = data.replace(/(.{32})/g, "$1\n");
wm.copyTextToClipboard(data);
clipboard.setText(data);
}
};
@@ -305,7 +308,7 @@ export function Switch() {
if (active < 0 || active >= __state.model.ports.length) {
$("switch-active-port").innerText = "N/A";
} else {
$("switch-active-port").innerText = "p" + __formatPort(__state.model, active);
$("switch-active-port").innerText = "p" + summary.active_id;
}
for (let port = 0; port < __state.model.ports.length; ++port) {
__setLedState($(`__switch-port-led-p${port}`), "green", (port === active));
@@ -333,7 +336,7 @@ export function Switch() {
let content = "";
let unit = -1;
for (let port = 0; port < model.ports.length; ++port) {
let pa = model.ports[port]; // pa == port attrs
let pa = model.ports[port]; // pa === port attrs
if (unit !== pa.unit) {
unit = pa.unit;
content += `${unit > 0 ? "<tr><td colspan=100><hr></td></tr>" : ""}
@@ -344,11 +347,11 @@ export function Switch() {
<td colspan=100>
<div class="buttons-row">
<button id="__switch-beacon-button-u${unit}" class="small" title="Toggle uplink Beacon Led">
<img id="__switch-beacon-led-u${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
<img id="__switch-beacon-led-u${unit}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-beacon.svg"/>
Uplink
</button>
<button id="__switch-beacon-button-d${unit}" class="small" title="Toggle downlink Beacon Led">
<img id="__switch-beacon-led-d${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
<img id="__switch-beacon-led-d${unit}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-beacon.svg"/>
Downlink
</button>
</div>
@@ -360,15 +363,15 @@ export function Switch() {
content += `
<tr>
<td>Port:</td>
<td class="value">${__formatPort(model, port)}</td>
<td class="value">${pa.id}</td>
<td>&nbsp;&nbsp;</td>
<td>
<div class="buttons-row">
<button id="__switch-port-button-p${port}" title="Activate this port">
<img id="__switch-port-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-circle.svg"/>
<img id="__switch-port-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-circle.svg"/>
</button>
<button id="__switch-params-button-p${port}" title="Configure this port">
<img id="__switch-params-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-gear.svg"/>
<img id="__switch-params-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-gear.svg"/>
</button>
</div>
</td>
@@ -385,14 +388,14 @@ export function Switch() {
</td>
<td style="font-size:1em">
<button id="__switch-beacon-button-p${port}" class="small" title="Toggle Beacon Led on this port">
<img id="__switch-beacon-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
<img id="__switch-beacon-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-beacon.svg"/>
</button>
</td>
<td>
<img id="__switch-video-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-video.svg" title="Video Link"/>
<img id="__switch-usb-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-usb.svg" title="USB Link"/>
<img id="__switch-atx-power-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-power.svg" title="Power Led"/>
<img id="__switch-atx-hdd-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-hdd.svg" title="HDD Led"/>
<img id="__switch-video-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-video.svg" title="Video Link"/>
<img id="__switch-usb-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-usb.svg" title="USB Link"/>
<img id="__switch-atx-power-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-atx-power.svg" title="Power Led"/>
<img id="__switch-atx-hdd-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-atx-hdd.svg" title="HDD Led"/>
</td>
<td>
<div class="buttons-row">
@@ -407,7 +410,8 @@ export function Switch() {
$("switch-chain").innerHTML = content;
if (model.units.length > 0) {
tools.hidden.setVisible($("switch-message-update"), (model.firmware.version > model.units[0].firmware.version));
let fw = model.units[0].firmware;
tools.hidden.setVisible($("switch-message-update"), (fw.devbuild || fw.version < model.firmware.version));
}
for (let unit = 0; unit < model.units.length; ++unit) {
@@ -437,6 +441,7 @@ export function Switch() {
let model = __state.model;
let edids = __state.edids;
let pa = model.ports[port]; // Port attrs
let atx_actions = {
"power": "ATX power click",
@@ -457,12 +462,12 @@ export function Switch() {
let create_content = function(el_parent) {
let html = `
<table>
<table style="width: 100%">
<tr>
<td>Port name:</td>
<td><input
type="text" autocomplete="off" id="__switch-port-name-input"
value="${tools.escape(model.ports[port].name)}" placeholder="Host ${port + 1}"
value="${tools.escape(pa.name)}" placeholder="Host ${port + 1}"
style="width:100%"
/></td>
</tr>
@@ -471,9 +476,22 @@ export function Switch() {
<td><select id="__switch-port-edid-selector" style="width: 100%"></select></td>
</tr>
</table>
<hr>
<table>
`;
let fw = model.units[pa.unit].firmware;
if (fw.devbuild || fw.version >= 8) {
html += `
<hr>
<table style="width: 100%">
<tr>
<td>Simulate display on inactive port:</td>
<td align="right">${tools.sw.makeItem("__switch-port-dummy-switch", pa.video.dummy)}</td>
</tr>
</table>
`;
}
html += "<hr><table style=\"width: 100%\">";
for (let kv of Object.entries(atx_actions)) {
html += `
<tr>
@@ -489,6 +507,7 @@ export function Switch() {
`;
}
html += "</table>";
el_parent.innerHTML = html;
let el_selector = $("__switch-port-edid-selector");
@@ -509,34 +528,30 @@ export function Switch() {
let reset_default = tools.partial(function(el_slider, limits) {
tools.slider.setValue(el_slider, limits["default"]);
}, el_slider, limits);
tools.slider.setParams(el_slider, limits.min, limits.max, 0.5, model.ports[port].atx.click_delays[action], display_value);
tools.slider.setParams(el_slider, limits.min, limits.max, 0.5, pa.atx.click_delays[action], display_value);
tools.el.setOnClick($(`__switch-port-atx-click-${action}-delay-default-button`), reset_default);
}
};
wm.modal(`Port ${__formatPort(__state.model, port)} settings`, create_content, true, true).then(function(ok) {
wm.modal(`Port ${pa.id} settings`, create_content, true, true).then(function(ok) {
if (ok) {
let params = {
"port": port,
"edid_id": $("__switch-port-edid-selector").value,
"name": $("__switch-port-name-input").value,
};
let el_dummy_switch = $("__switch-port-dummy-switch");
if (el_dummy_switch) { // Only for devbuild or firmware >= 8
params["dummy"] = $("__switch-port-dummy-switch").checked;
}
for (let action of Object.keys(atx_actions)) {
params[`atx_click_${action}_delay`] = tools.slider.getValue($(`__switch-port-atx-click-${action}-delay-slider`));
};
__sendPost("/api/switch/set_port_params", params);
__sendPost("api/switch/set_port_params", params);
}
});
};
var __formatPort = function(model, port) {
if (model.units.length > 1) {
return `${model.ports[port].unit + 1}.${model.ports[port].channel + 1}`;
} else {
return `${port + 1}`;
}
};
var __setLedState = function(el, color, on) {
el.classList.toggle(`led-${color}`, on);
el.classList.toggle("led-gray", !on);
@@ -549,41 +564,50 @@ export function Switch() {
};
var __switchActivePort = function(port) {
let switch_port = () => __sendPost("api/switch/set_active", {"port": port});
if (__msd_connected) {
wm.error(`
Oops! Before port switching, please disconnect an active Mass Storage Drive image first.
Otherwise, it will break a current USB operation (OS installation, Live CD, or whatever).
`);
if ($("switch-msd-ask-switch").checked) {
wm.confirm(`
The Mass Storage Drive is active.<br><br>
If you switch the port now, it will break a current USB disk operation<br>
(OS installation, Live CD, or whatever).<br><br>
Are you sure you want to continue a port switching?
`).then(function(ok) {
if (ok) {
switch_port();
}
});
} else {
switch_port();
}
} else {
__sendPost("/api/switch/set_active", {"port": port});
switch_port();
}
};
var __switchUplinkBeacon = function(unit) {
let state = false;
try { state = !__state.beacons.uplinks[unit]; } catch {}; // eslint-disable-line no-empty
__sendPost("/api/switch/set_beacon", {"uplink": unit, "state": state});
__sendPost("api/switch/set_beacon", {"uplink": unit, "state": state});
};
var __switchDownlinkBeacon = function(unit) {
let state = false;
try { state = !__state.beacons.downlinks[unit]; } catch {}; // eslint-disable-line no-empty
__sendPost("/api/switch/set_beacon", {"downlink": unit, "state": state});
__sendPost("api/switch/set_beacon", {"downlink": unit, "state": state});
};
var __switchPortBeacon = function(port) {
let state = false;
try { state = !__state.beacons.ports[port]; } catch {}; // eslint-disable-line no-empty
__sendPost("/api/switch/set_beacon", {"port": port, "state": state});
__sendPost("api/switch/set_beacon", {"port": port, "state": state});
};
var __atxClick = function(port, button) {
let click_button = function() {
__sendPost("/api/switch/atx/click", {"port": port, "button": button});
};
let click_button = () => __sendPost("api/switch/atx/click", {"port": port, "button": button});
if ($("switch-atx-ask-switch").checked) {
wm.confirm(`
Are you sure you want to press the <b>${button}</b> button?<br>
Are you sure you want to press the <b>${tools.escape(button)}</b> button?<br>
Warning! This could cause data loss on the server.
`).then(function(ok) {
if (ok) {

View File

@@ -32,10 +32,17 @@ export function main() {
if (checkBrowser(null, null)) {
initWindowManager();
// Radio is a string container
tools.radio.clickValue("expire-radio", tools.storage.get("login.expire", 0));
tools.radio.setOnClick("expire-radio", function() {
let expire = parseInt(tools.radio.getValue("expire-radio"));
tools.storage.setInt("login.expire", expire);
}, false);
tools.el.setOnClick($("login-button"), __login);
$("user-input").onkeyup = $("passwd-input").onkeyup = $("code-input").onkeyup = function(event) {
if (event.code === "Enter") {
event.preventDefault();
$("user-input").onkeyup = $("passwd-input").onkeyup = $("code-input").onkeyup = function(ev) {
if (ev.code === "Enter") {
ev.preventDefault();
$("login-button").click();
}
};
@@ -45,31 +52,43 @@ export function main() {
}
function __login() {
let user = $("user-input").value;
if (user.length === 0) {
let e_user = encodeURIComponent($("user-input").value);
if (e_user.length === 0) {
$("user-input").focus();
} else {
let passwd = $("passwd-input").value + $("code-input").value;
let body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`;
tools.httpPost("/api/auth/login", null, function(http) {
if (http.status === 200) {
document.location.href = "/";
} else if (http.status === 403) {
wm.error("Invalid credentials").then(__tryAgain);
} else {
return;
}
let e_passwd = encodeURIComponent($("passwd-input").value + $("code-input").value);
let e_expire = encodeURIComponent(tools.radio.getValue("expire-radio"));
let body = `user=${e_user}&passwd=${e_passwd}&expire=${e_expire}`;
tools.httpPost("api/auth/login", null, function(http) {
switch (http.status) {
case 200:
tools.currentOpen("");
break;
case 403:
wm.error("Invalid username, password, or OTP").then(__tryAgain);
break;
default: {
let error = "";
if (http.status === 400) {
try { error = JSON.parse(http.responseText)["result"]["error"]; } catch { /* Nah */ }
try {
error = JSON.parse(http.responseText)["result"]["error"];
} catch { /* Nah */ }
}
if (error === "ValidatorError") {
wm.error("Invalid characters in credentials").then(__tryAgain);
} else {
wm.error("Login error", http.responseText).then(__tryAgain);
wm.error("Unexpected login error:", http.responseText).then(__tryAgain);
}
}
}, body, "application/x-www-form-urlencoded");
__setEnabled(false);
}
} break;
}
}, body, "application/x-www-form-urlencoded");
__setEnabled(false);
}
function __setEnabled(enabled) {

View File

@@ -23,6 +23,7 @@
"use strict";
import {ROOT_PREFIX} from "./vars.js";
import {browser} from "./bb.js";
@@ -39,7 +40,16 @@ export var tools = new function() {
/************************************************************************/
self.currentOpen = function(url) {
window.location.href = ROOT_PREFIX + url;
};
self.windowOpen = function(url) {
window.open(ROOT_PREFIX + url, "_blank");
};
self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) {
url = ROOT_PREFIX + url;
if (params) {
params = new URLSearchParams(params);
if (params) {
@@ -68,11 +78,19 @@ export var tools = new function() {
self.httpRequest("POST", url, params, callback, body, content_type, timeout);
};
self.makeWsUrl = function(url) {
let proto = (self.is_https ? "wss://" : "ws://");
return proto + window.location.host + window.location.pathname + ROOT_PREFIX + url;
};
/************************************************************************/
self.escape = function(text) {
if (typeof text !== "string") {
text = "" + text;
}
return text.replace(
/[^0-9A-Za-z ]/g,
/[^-_0-9A-Za-z ]/g,
ch => "&#" + ch.charCodeAt(0) + ";"
);
};
@@ -85,7 +103,7 @@ export var tools = new function() {
return text[0].toUpperCase() + text.slice(1);
};
self.makeId = function() {
self.makeRandomId = function() {
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let id = "";
for (let count = 0; count < 16; ++count) {
@@ -94,16 +112,10 @@ export var tools = new function() {
return id;
};
self.makeIdByText = function(text) {
self.makeTextId = function(text) {
return btoa(text).replace("=", "_");
};
self.getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
self.formatSize = function(size) {
if (size > 0) {
let index = Math.floor( Math.log(size) / Math.log(1024) );
@@ -124,14 +136,15 @@ export var tools = new function() {
return `${hours}:${mins}:${secs}.${millis}`;
};
self.remap = function(x, a1, b1, a2, b2) {
let remapped = Math.round((x - a1) / b1 * (b2 - a2) + a2);
if (remapped < a2) {
return a2;
} else if (remapped > b2) {
return b2;
}
return remapped;
self.remap = function(value, in_min, in_max, out_min, out_max) {
let result = Math.round((value - in_min) * (out_max - out_min) / ((in_max - in_min) || 1) + out_min);
return Math.min(Math.max(result, out_min), out_max);
};
self.getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/************************************************************************/
@@ -139,25 +152,25 @@ export var tools = new function() {
self.el = new function() {
return {
"setOnClick": function(el, callback, prevent_default=true) {
el.onclick = el.ontouchend = function(event) {
el.onclick = el.ontouchend = function(ev) {
if (prevent_default) {
event.preventDefault();
ev.preventDefault();
}
callback();
};
},
"setOnDown": function(el, callback, prevent_default=true) {
el.onmousedown = el.ontouchstart = function(event) {
el.onmousedown = el.ontouchstart = function(ev) {
if (prevent_default) {
event.preventDefault();
ev.preventDefault();
}
callback();
callback(ev);
};
},
"setOnUp": function(el, callback, prevent_default=true) {
el.onmouseup = el.ontouchend = function(event) {
el.onmouseup = el.ontouchend = function(ev) {
if (prevent_default) {
event.preventDefault();
ev.preventDefault();
}
callback();
};
@@ -197,9 +210,9 @@ export var tools = new function() {
el.__pressed = true;
};
el.onmouseup = el.ontouchend = function(event) {
el.onmouseup = el.ontouchend = function(ev) {
let value = self.slider.getValue(el);
event.preventDefault();
ev.preventDefault();
clear_timer();
el.__execution_timer = setTimeout(function() {
el.__pressed = false;
@@ -252,29 +265,57 @@ export var tools = new function() {
};
};
self.sw = new function() {
return {
"makeItem": function(id, checked) {
id = tools.escape(id);
return `
<div class="switch-box">
<input
type="checkbox" id="${id}"
${checked ? "checked" : ""}
/>
<label for="${id}">
<span class="switch-inner"></span>
<span class="switch"></span>
</label>
</div>
`;
},
};
};
self.radio = new function() {
return {
"makeItem": function(name, title, value) {
let e_id = self.escape(name) + self.makeTextId(value);
return `
<input type="radio" id="${name}-${value}" name="${name}" value="${value}" />
<label for="${name}-${value}">${title}</label>
<input
type="radio"
id="${e_id}"
name="${tools.escape(name)}"
value="${tools.escape(value)}"
/>
<label for="${e_id}">
${tools.escape(title)}
</label>
`;
},
"setOnClick": function(name, callback, prevent_default=true) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
self.el.setOnClick(el, callback, prevent_default);
}
},
"getValue": function(name) {
return document.querySelector(`input[type="radio"][name="${name}"]:checked`).value;
return document.querySelector(`input[type="radio"][name="${CSS.escape(name)}"]:checked`).value;
},
"setValue": function(name, value) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
el.checked = (el.value === value);
}
},
"clickValue": function(name, value) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
if (el.value === value) {
el.click();
return;
@@ -282,7 +323,7 @@ export var tools = new function() {
}
},
"setEnabled": function(name, enabled) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
self.el.setEnabled(el, enabled);
}
},
@@ -359,7 +400,9 @@ export var tools = new function() {
self.feature = new function() {
return {
"setEnabled": function(el, enabled) {
el.classList.toggle("feature-disabled", !enabled);
if (el) {
el.classList.toggle("feature-disabled", !enabled);
}
},
};
};
@@ -383,7 +426,7 @@ export var tools = new function() {
/************************************************************************/
self.is_https = (location.protocol === "https:");
self.is_https = (window.location.protocol === "https:");
self.cookies = new function() {
return {

31
web/share/js/vars.js Normal file
View File

@@ -0,0 +1,31 @@
/*****************************************************************************
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
*****************************************************************************/
"use strict";
export var ROOT_PREFIX = "./";
export function setRootPrefix(prefix) {
ROOT_PREFIX = prefix;
}

View File

@@ -31,17 +31,27 @@ export function main() {
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", null, function(http) {
if (http.status === 200) {
let vnc_port = JSON.parse(http.responseText).result.extras.vnc.port;
$("vnc-text").innerHTML = `
<span class="code-comment"># How to connect using the Linux terminal:<br>
$</span> vncviewer ${window.location.hostname}::${vnc_port}
`;
} else if (http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
setTimeout(__loadKvmdInfo, 1000);
tools.httpGet("api/info", null, function(http) {
switch (http.status) {
case 200:
__showKvmdInfo(JSON.parse(http.responseText).result);
break;
case 401:
case 403:
tools.currentOpen("login");
break;
default:
setTimeout(__loadKvmdInfo, 1000);
break;
}
});
}
function __showKvmdInfo(info) {
$("vnc-text").innerHTML = `
<span class="code-comment"># How to connect using the Linux terminal:<br>
$</span> vncviewer ${tools.escape(window.location.hostname + "::" + info.extras.vnc.port)}
`;
}

File diff suppressed because it is too large Load Diff