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":
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),
},

View File

@ -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()),
})

View File

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

View File

@ -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,
}

View File

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

View File

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

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-atx.pug
include navbar-msd.pug
include navbar-gpio.pug
include navbar-macro.pug
include navbar-shortcuts.pug

View File

@ -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;

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 {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);