summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--kvmd/apps/__init__.py4
-rw-r--r--kvmd/apps/kvmd/api/ugpio.py3
-rw-r--r--kvmd/apps/kvmd/server.py3
-rw-r--r--kvmd/apps/kvmd/ugpio.py56
-rw-r--r--testenv/v2-hdmi-rpi4.override.yaml71
-rw-r--r--web/kvm/index.html3
-rw-r--r--web/kvm/navbar-gpio.pug4
-rw-r--r--web/kvm/navbar.pug1
-rw-r--r--web/share/css/main.css7
-rw-r--r--web/share/js/kvm/gpio.js163
-rw-r--r--web/share/js/kvm/session.js5
11 files changed, 305 insertions, 15 deletions
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py
index 28fbfc75..7202cfbc 100644
--- a/kvmd/apps/__init__.py
+++ b/kvmd/apps/__init__.py
@@ -187,6 +187,7 @@ def _patch_dynamic( # pylint: disable=too-many-locals
}
if mode == "output":
ch_scheme.update({
+ "busy_delay": Option(0.2, type=valid_float_f01),
"initial": Option(False, type=valid_bool),
"switch": Option(True, type=valid_bool),
"pulse": {
@@ -328,8 +329,7 @@ def _get_config_scheme() -> Dict:
"scheme": {}, # Dymanic content
"view": {
"header": {
- "title": Option("Switches"),
- "leds": Option([], type=valid_string_list),
+ "title": Option("GPIO"),
},
"table": Option([], type=valid_ugpio_view_table),
},
diff --git a/kvmd/apps/kvmd/api/ugpio.py b/kvmd/apps/kvmd/api/ugpio.py
index 69ac6646..66f7b358 100644
--- a/kvmd/apps/kvmd/api/ugpio.py
+++ b/kvmd/apps/kvmd/api/ugpio.py
@@ -44,8 +44,7 @@ class UserGpioApi:
@exposed_http("GET", "/gpio")
async def __state_handler(self, _: Request) -> Response:
return make_json_response({
- "scheme": (await self.__user_gpio.get_scheme()),
- "view": (await self.__user_gpio.get_view()),
+ "model": (await self.__user_gpio.get_model()),
"state": (await self.__user_gpio.get_state()),
})
diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py
index 5208f25d..3f1361aa 100644
--- a/kvmd/apps/kvmd/server.py
+++ b/kvmd/apps/kvmd/server.py
@@ -243,8 +243,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
await client.ws.prepare(request)
await self.__register_ws_client(client)
try:
- await self.__broadcast_event("gpio_scheme_state", await self.__user_gpio.get_scheme())
- await self.__broadcast_event("gpio_view_state", await self.__user_gpio.get_view())
+ await self.__broadcast_event("gpio_model_state", await self.__user_gpio.get_model())
await asyncio.gather(*[
self.__broadcast_event(component.event_type, await component.get_state())
for component in self.__components
diff --git a/kvmd/apps/kvmd/ugpio.py b/kvmd/apps/kvmd/ugpio.py
index 97556aba..c6cfc121 100644
--- a/kvmd/apps/kvmd/ugpio.py
+++ b/kvmd/apps/kvmd/ugpio.py
@@ -23,6 +23,7 @@
import asyncio
import operator
+from typing import List
from typing import Dict
from typing import AsyncGenerator
from typing import Optional
@@ -88,6 +89,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
self.__pulse_delay: float = config.pulse.delay
self.__min_pulse_delay: float = config.pulse.min_delay
self.__max_pulse_delay: float = config.pulse.max_delay
+ self.__busy_delay: float = config.busy_delay
self.__state = config.initial
self.__region = aiotools.AioExclusiveRegion(GpioChannelIsBusyError, notifier)
@@ -97,8 +99,8 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
"switch": self.__switch,
"pulse": {
"delay": self.__pulse_delay,
- "min_delay": self.__min_pulse_delay,
- "max_delay": self.__max_pulse_delay,
+ "min_delay": (self.__min_pulse_delay if self.__pulse_delay else 0),
+ "max_delay": (self.__max_pulse_delay if self.__pulse_delay else 0),
},
}
@@ -125,8 +127,10 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
self.__write(state)
self.__state = state
get_logger(0).info("Switched GPIO %s to %d", self, state)
+ await asyncio.sleep(self.__busy_delay)
return True
self.__state = real_state
+ await asyncio.sleep(self.__busy_delay)
return False
@aiotools.atomic
@@ -146,7 +150,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
await asyncio.sleep(delay)
finally:
self.__write(False)
- await asyncio.sleep(1)
+ await asyncio.sleep(self.__busy_delay)
get_logger(0).info("Pulsed GPIO %s", self)
def __read(self) -> bool:
@@ -185,15 +189,15 @@ class UserGpio:
else: # output:
self.__outputs[channel] = _GpioOutput(channel, ch_config, self.__state_notifier)
- async def get_scheme(self) -> Dict:
+ async def get_model(self) -> Dict:
return {
- "inputs": {channel: gin.get_scheme() for (channel, gin) in self.__inputs.items()},
- "outputs": {channel: gout.get_scheme() for (channel, gout) in self.__outputs.items()},
+ "scheme": {
+ "inputs": {channel: gin.get_scheme() for (channel, gin) in self.__inputs.items()},
+ "outputs": {channel: gout.get_scheme() for (channel, gout) in self.__outputs.items()},
+ },
+ "view": self.__make_view(),
}
- async def get_view(self) -> Dict:
- return self.__view
-
async def get_state(self) -> Dict:
return {
"inputs": {channel: gin.get_state() for (channel, gin) in self.__inputs.items()},
@@ -240,3 +244,37 @@ class UserGpio:
if gout is None:
raise GpioChannelNotFoundError()
await gout.pulse(delay)
+
+ # =====
+
+ def __make_view(self) -> Dict:
+ table: List[Optional[List[Dict]]] = []
+ for row in self.__view["table"]:
+ if len(row) == 0:
+ table.append(None)
+ continue
+
+ items: List[Dict] = []
+ for item in map(str.strip, row):
+ if item.startswith("#") or len(item) == 0:
+ items.append({
+ "type": "label",
+ "text": item[1:].strip(),
+ })
+ elif (parts := list(map(str.strip, item.split(",", 1)))):
+ if parts[0] in self.__inputs:
+ items.append({
+ "type": "input",
+ "channel": parts[0],
+ })
+ elif parts[0] in self.__outputs:
+ items.append({
+ "type": "output",
+ "channel": parts[0],
+ "text": (parts[1] if len(parts) > 1 else "Click"),
+ })
+ table.append(items)
+ return {
+ "header": self.__view["header"],
+ "table": table,
+ }
diff --git a/testenv/v2-hdmi-rpi4.override.yaml b/testenv/v2-hdmi-rpi4.override.yaml
index a297e78b..1977393a 100644
--- a/testenv/v2-hdmi-rpi4.override.yaml
+++ b/testenv/v2-hdmi-rpi4.override.yaml
@@ -37,6 +37,77 @@ kvmd:
- "--notify-parent"
- "--no-log-colors"
+ gpio:
+ scheme:
+ host1: # any name like foo_bar_baz
+ pin: 1
+ mode: input
+ host2:
+ pin: 2
+ mode: input
+ host3:
+ pin: 3
+ mode: input
+ host4:
+ pin: 4
+ mode: input
+ change_host:
+ pin: 5
+ mode: output
+ switch: false
+
+ host1_pwr:
+ pin: 11
+ mode: input
+ host2_pwr:
+ pin: 12
+ mode: input
+ host3_pwr:
+ pin: 13
+ mode: input
+ host4_pwr:
+ pin: 14
+ mode: input
+
+ host1_pwr_btn:
+ pin: 21
+ mode: output
+ switch: false
+ host2_pwr_btn:
+ pin: 22
+ mode: output
+ switch: false
+ host3_pwr_btn:
+ pin: 23
+ mode: output
+ switch: false
+ host4_pwr_btn:
+ pin: 24
+ mode: output
+ switch: false
+
+ lamp:
+ pin: 50
+ mode: output
+ pulse:
+ delay: 0
+
+ view:
+ header:
+ title: Switch
+ table:
+ - ["#Multihost controller"]
+ - []
+ - ["", "#Current", "#Power"]
+ - ["#host1.localdomain:", host1, host1_pwr, "host1_pwr_btn,Pwr"]
+ - ["#host2.localdomain:", host2, host2_pwr, "host2_pwr_btn,Pwr"]
+ - ["#host3.localdomain:", host3, host3_pwr, "host3_pwr_btn,Pwr"]
+ - ["#host4.localdomain:", host4, host4_pwr, "host4_pwr_btn,Pwr"]
+ - []
+ - ["change_host,Change host"]
+ - []
+ - ["#Lamp in the rack", lamp]
+
vnc:
keymap: /usr/share/kvmd/keymaps/ru
diff --git a/web/kvm/index.html b/web/kvm/index.html
index 727888ae..0c0b3ff0 100644
--- a/web/kvm/index.html
+++ b/web/kvm/index.html
@@ -328,6 +328,9 @@
</div>
</div>
</li>
+ <li class="right feature-disabled" id="gpio-dropdown"><a class="menu-button" id="gpio-menu-button" href="#">GPIO &#8628;</a>
+ <div class="menu" data-dont-hide-menu id="gpio-menu"></div>
+ </li>
<li class="right"><a class="menu-button" href="#"><img class="led-gray" data-dont-hide-menu id="hid-recorder-led" src="/share/svg/led-gear.svg">Macro &#8628;</a>
<div class="menu" data-dont-hide-menu>
<div class="text"><b>Record and play keyboard &amp; mouse actions<br></b><sub>For security reasons, the record will not saved on Pi-KVM</sub></div>
diff --git a/web/kvm/navbar-gpio.pug b/web/kvm/navbar-gpio.pug
new file mode 100644
index 00000000..280ba3fd
--- /dev/null
+++ b/web/kvm/navbar-gpio.pug
@@ -0,0 +1,4 @@
+li(id="gpio-dropdown" class="right feature-disabled")
+ a(class="menu-button" id="gpio-menu-button" href="#")
+ | GPIO &#8628;
+ div(data-dont-hide-menu id="gpio-menu" class="menu")
diff --git a/web/kvm/navbar.pug b/web/kvm/navbar.pug
index d7743160..422cf236 100644
--- a/web/kvm/navbar.pug
+++ b/web/kvm/navbar.pug
@@ -23,5 +23,6 @@ ul(id="navbar")
include navbar-system.pug
include navbar-atx.pug
include navbar-msd.pug
+ include navbar-gpio.pug
include navbar-macro.pug
include navbar-shortcuts.pug
diff --git a/web/share/css/main.css b/web/share/css/main.css
index 443b637a..cfba1c11 100644
--- a/web/share/css/main.css
+++ b/web/share/css/main.css
@@ -94,6 +94,13 @@ img.inline-lamp {
margin-right: 2px;
}
+img.inline-lamp-big {
+ vertical-align: middle;
+ height: 16px;
+ margin-left: 2px;
+ margin-right: 2px;
+}
+
button,
select {
border: none;
diff --git a/web/share/js/kvm/gpio.js b/web/share/js/kvm/gpio.js
new file mode 100644
index 00000000..53644038
--- /dev/null
+++ b/web/share/js/kvm/gpio.js
@@ -0,0 +1,163 @@
+/*****************************************************************************
+# #
+# KVMD - The main Pi-KVM daemon. #
+# #
+# Copyright (C) 2018 Maxim Devaev <[email protected]> #
+# #
+# 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 function Gpio() {
+ var self = this;
+
+ /************************************************************************/
+
+ var __state = null;
+
+ /************************************************************************/
+
+ self.setState = function(state) {
+ if (state) {
+ for (let channel in state.inputs) {
+ let el = $(`gpio-led-${channel}`);
+ if (el) {
+ __setLedState(el, state.inputs[channel].state);
+ }
+ }
+ for (let channel in state.outputs) {
+ for (let type of ["switch", "button"]) {
+ let el = $(`gpio-${type}-${channel}`);
+ if (el) {
+ wm.switchEnabled(el, !state.outputs[channel].busy);
+ }
+ }
+ }
+ } else {
+ for (let el of $$$(".gpio-led")) {
+ __setLedState(el, false);
+ }
+ for (let selector of [".gpio-switch", ".gpio-button"]) {
+ for (let el of $$$(selector)) {
+ wm.switchEnabled(el, false);
+ }
+ }
+ }
+ __state = state;
+ };
+
+ self.setModel = function(model) {
+ tools.featureSetEnabled($("gpio-dropdown"), model.view.table.length);
+ if (model.view.table.length) {
+ $("gpio-menu-button").innerHTML = `${model.view.header.title} &#8628;`;
+ }
+
+ let switches = [];
+ let buttons = [];
+ let content = "<table class=\"kv\">";
+ for (let row of model.view.table) {
+ if (row === null) {
+ content += "</table><hr><table class=\"kv\">";
+ } else {
+ content += "<tr>";
+ for (let item of row) {
+ if (item.type === "output") {
+ item.scheme = model.scheme.outputs[item.channel];
+ }
+ content += `<td>${__createItem(item, switches, buttons)}</td>`;
+ }
+ content += "</tr>";
+ }
+ }
+ content += "</table>";
+ $("gpio-menu").innerHTML = content;
+
+ for (let channel of switches) {
+ tools.setOnClick($(`gpio-switch-${channel}`), () => __switchChannel(channel));
+ }
+ for (let channel of buttons) {
+ tools.setOnClick($(`gpio-button-${channel}`), () => __pulseChannel(channel));
+ }
+
+ self.setState(__state);
+ };
+
+ var __createItem = function(item, switches, buttons) {
+ if (item.type === "label") {
+ return item.text;
+ } else if (item.type === "input") {
+ return `<img id="gpio-led-${item.channel}" class="gpio-led inline-lamp-big led-gray" src="/share/svg/led-square.svg" />`;
+ } else if (item.type === "output") {
+ let controls = [];
+ if (item.scheme["switch"]) {
+ switches.push(item.channel);
+ controls.push(`
+ <td><div class="switch-box">
+ <input disabled type="checkbox" id="gpio-switch-${item.channel}" class="gpio-switch" />
+ <label for="gpio-switch-${item.channel}">
+ <span class="switch-inner"></span>
+ <span class="switch"></span>
+ </label>
+ </div></td>
+ `);
+ }
+ if (item.scheme.pulse.delay) {
+ buttons.push(item.channel);
+ controls.push(`<td><button disabled id="gpio-button-${item.channel}" class="gpio-button">${item.text}</button></td>`);
+ }
+ return `<table><tr>${controls.join("<td>&nbsp;&nbsp;&nbsp;</td>")}</tr></table>`;
+ } else {
+ return "";
+ }
+ };
+
+ var __setLedState = function(el, state) {
+ if (state) {
+ el.classList.add("led-green");
+ el.classList.remove("led-gray");
+ } else {
+ el.classList.add("led-gray");
+ el.classList.remove("led-green");
+ }
+ };
+
+ var __switchChannel = function(channel) {
+ let to = ($(`gpio-switch-${channel}`).checked ? "1" : "0");
+ __sendPost(`/api/gpio/switch?channel=${channel}&state=${to}`);
+ };
+
+ var __pulseChannel = function(channel) {
+ __sendPost(`/api/gpio/pulse?channel=${channel}`);
+ };
+
+ var __sendPost = function(url) {
+ let http = tools.makeRequest("POST", url, function() {
+ if (http.readyState === 4) {
+ if (http.status === 409) {
+ wm.error("Performing another operation for this GPIO channel.<br>Please try again later");
+ } else if (http.status !== 200) {
+ wm.error("GPIO error:<br>", http.responseText);
+ }
+ }
+ });
+ };
+}
diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js
index 06ffcd95..7112d51c 100644
--- a/web/share/js/kvm/session.js
+++ b/web/share/js/kvm/session.js
@@ -31,6 +31,7 @@ import {Atx} from "./atx.js";
import {Msd} from "./msd.js";
import {Streamer} from "./stream.js";
import {WakeOnLan} from "./wol.js";
+import {Gpio} from "./gpio.js";
export function Session() {
@@ -48,6 +49,7 @@ export function Session() {
var __msd = new Msd();
var __streamer = new Streamer();
var __wol = new WakeOnLan();
+ var __gpio = new Gpio();
var __init__ = function() {
__startSession();
@@ -211,6 +213,8 @@ export function Session() {
case "info_hw_state": __setAboutInfoHw(data.event); break;
case "info_system_state": __setAboutInfoSystem(data.event); break;
case "wol_state": __wol.setState(data.event); break;
+ case "gpio_model_state": __gpio.setModel(data.event); break;
+ case "gpio_state": __gpio.setState(data.event); break;
case "hid_state": __hid.setState(data.event); break;
case "atx_state": __atx.setState(data.event); break;
case "msd_state": __msd.setState(data.event); break;
@@ -237,6 +241,7 @@ export function Session() {
__ping_timer = null;
}
+ __gpio.setState(null);
__hid.setSocket(null);
__atx.setState(null);
__msd.setState(null);