This commit is contained in:
Devaev Maxim 2020-09-03 06:51:02 +03:00
parent 5307765399
commit 68ab7ce33c
11 changed files with 305 additions and 15 deletions

View File

@ -187,6 +187,7 @@ def _patch_dynamic( # pylint: disable=too-many-locals
} }
if mode == "output": if mode == "output":
ch_scheme.update({ ch_scheme.update({
"busy_delay": Option(0.2, type=valid_float_f01),
"initial": Option(False, type=valid_bool), "initial": Option(False, type=valid_bool),
"switch": Option(True, type=valid_bool), "switch": Option(True, type=valid_bool),
"pulse": { "pulse": {
@ -328,8 +329,7 @@ def _get_config_scheme() -> Dict:
"scheme": {}, # Dymanic content "scheme": {}, # Dymanic content
"view": { "view": {
"header": { "header": {
"title": Option("Switches"), "title": Option("GPIO"),
"leds": Option([], type=valid_string_list),
}, },
"table": Option([], type=valid_ugpio_view_table), "table": Option([], type=valid_ugpio_view_table),
}, },

View File

@ -44,8 +44,7 @@ class UserGpioApi:
@exposed_http("GET", "/gpio") @exposed_http("GET", "/gpio")
async def __state_handler(self, _: Request) -> Response: async def __state_handler(self, _: Request) -> Response:
return make_json_response({ return make_json_response({
"scheme": (await self.__user_gpio.get_scheme()), "model": (await self.__user_gpio.get_model()),
"view": (await self.__user_gpio.get_view()),
"state": (await self.__user_gpio.get_state()), "state": (await self.__user_gpio.get_state()),
}) })

View File

@ -243,8 +243,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins
await client.ws.prepare(request) await client.ws.prepare(request)
await self.__register_ws_client(client) await self.__register_ws_client(client)
try: try:
await self.__broadcast_event("gpio_scheme_state", await self.__user_gpio.get_scheme()) await self.__broadcast_event("gpio_model_state", await self.__user_gpio.get_model())
await self.__broadcast_event("gpio_view_state", await self.__user_gpio.get_view())
await asyncio.gather(*[ await asyncio.gather(*[
self.__broadcast_event(component.event_type, await component.get_state()) self.__broadcast_event(component.event_type, await component.get_state())
for component in self.__components for component in self.__components

View File

@ -23,6 +23,7 @@
import asyncio import asyncio
import operator import operator
from typing import List
from typing import Dict from typing import Dict
from typing import AsyncGenerator from typing import AsyncGenerator
from typing import Optional from typing import Optional
@ -88,6 +89,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
self.__pulse_delay: float = config.pulse.delay self.__pulse_delay: float = config.pulse.delay
self.__min_pulse_delay: float = config.pulse.min_delay self.__min_pulse_delay: float = config.pulse.min_delay
self.__max_pulse_delay: float = config.pulse.max_delay self.__max_pulse_delay: float = config.pulse.max_delay
self.__busy_delay: float = config.busy_delay
self.__state = config.initial self.__state = config.initial
self.__region = aiotools.AioExclusiveRegion(GpioChannelIsBusyError, notifier) self.__region = aiotools.AioExclusiveRegion(GpioChannelIsBusyError, notifier)
@ -97,8 +99,8 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
"switch": self.__switch, "switch": self.__switch,
"pulse": { "pulse": {
"delay": self.__pulse_delay, "delay": self.__pulse_delay,
"min_delay": self.__min_pulse_delay, "min_delay": (self.__min_pulse_delay if self.__pulse_delay else 0),
"max_delay": self.__max_pulse_delay, "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.__write(state)
self.__state = state self.__state = state
get_logger(0).info("Switched GPIO %s to %d", self, state) get_logger(0).info("Switched GPIO %s to %d", self, state)
await asyncio.sleep(self.__busy_delay)
return True return True
self.__state = real_state self.__state = real_state
await asyncio.sleep(self.__busy_delay)
return False return False
@aiotools.atomic @aiotools.atomic
@ -146,7 +150,7 @@ class _GpioOutput: # pylint: disable=too-many-instance-attributes
await asyncio.sleep(delay) await asyncio.sleep(delay)
finally: finally:
self.__write(False) self.__write(False)
await asyncio.sleep(1) await asyncio.sleep(self.__busy_delay)
get_logger(0).info("Pulsed GPIO %s", self) get_logger(0).info("Pulsed GPIO %s", self)
def __read(self) -> bool: def __read(self) -> bool:
@ -185,15 +189,15 @@ class UserGpio:
else: # output: else: # output:
self.__outputs[channel] = _GpioOutput(channel, ch_config, self.__state_notifier) self.__outputs[channel] = _GpioOutput(channel, ch_config, self.__state_notifier)
async def get_scheme(self) -> Dict: async def get_model(self) -> Dict:
return { return {
"inputs": {channel: gin.get_scheme() for (channel, gin) in self.__inputs.items()}, "scheme": {
"outputs": {channel: gout.get_scheme() for (channel, gout) in self.__outputs.items()}, "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: async def get_state(self) -> Dict:
return { return {
"inputs": {channel: gin.get_state() for (channel, gin) in self.__inputs.items()}, "inputs": {channel: gin.get_state() for (channel, gin) in self.__inputs.items()},
@ -240,3 +244,37 @@ class UserGpio:
if gout is None: if gout is None:
raise GpioChannelNotFoundError() raise GpioChannelNotFoundError()
await gout.pulse(delay) 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,
}

View File

@ -37,6 +37,77 @@ kvmd:
- "--notify-parent" - "--notify-parent"
- "--no-log-colors" - "--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: vnc:
keymap: /usr/share/kvmd/keymaps/ru keymap: /usr/share/kvmd/keymaps/ru

View File

@ -328,6 +328,9 @@
</div> </div>
</div> </div>
</li> </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> <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="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> <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>

4
web/kvm/navbar-gpio.pug Normal file
View File

@ -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")

View File

@ -23,5 +23,6 @@ ul(id="navbar")
include navbar-system.pug include navbar-system.pug
include navbar-atx.pug include navbar-atx.pug
include navbar-msd.pug include navbar-msd.pug
include navbar-gpio.pug
include navbar-macro.pug include navbar-macro.pug
include navbar-shortcuts.pug include navbar-shortcuts.pug

View File

@ -94,6 +94,13 @@ img.inline-lamp {
margin-right: 2px; margin-right: 2px;
} }
img.inline-lamp-big {
vertical-align: middle;
height: 16px;
margin-left: 2px;
margin-right: 2px;
}
button, button,
select { select {
border: none; border: none;

163
web/share/js/kvm/gpio.js Normal file
View File

@ -0,0 +1,163 @@
/*****************************************************************************
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 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 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);
}
}
});
};
}

View File

@ -31,6 +31,7 @@ import {Atx} from "./atx.js";
import {Msd} from "./msd.js"; import {Msd} from "./msd.js";
import {Streamer} from "./stream.js"; import {Streamer} from "./stream.js";
import {WakeOnLan} from "./wol.js"; import {WakeOnLan} from "./wol.js";
import {Gpio} from "./gpio.js";
export function Session() { export function Session() {
@ -48,6 +49,7 @@ export function Session() {
var __msd = new Msd(); var __msd = new Msd();
var __streamer = new Streamer(); var __streamer = new Streamer();
var __wol = new WakeOnLan(); var __wol = new WakeOnLan();
var __gpio = new Gpio();
var __init__ = function() { var __init__ = function() {
__startSession(); __startSession();
@ -211,6 +213,8 @@ export function Session() {
case "info_hw_state": __setAboutInfoHw(data.event); break; case "info_hw_state": __setAboutInfoHw(data.event); break;
case "info_system_state": __setAboutInfoSystem(data.event); break; case "info_system_state": __setAboutInfoSystem(data.event); break;
case "wol_state": __wol.setState(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 "hid_state": __hid.setState(data.event); break;
case "atx_state": __atx.setState(data.event); break; case "atx_state": __atx.setState(data.event); break;
case "msd_state": __msd.setState(data.event); break; case "msd_state": __msd.setState(data.event); break;
@ -237,6 +241,7 @@ export function Session() {
__ping_timer = null; __ping_timer = null;
} }
__gpio.setState(null);
__hid.setSocket(null); __hid.setSocket(null);
__atx.setState(null); __atx.setState(null);
__msd.setState(null); __msd.setState(null);