mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
gpio ui
This commit is contained in:
parent
5307765399
commit
68ab7ce33c
@ -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),
|
||||
},
|
||||
|
||||
@ -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()),
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 ↴</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 ↴</a>
|
||||
<div class="menu" data-dont-hide-menu>
|
||||
<div class="text"><b>Record and play keyboard & 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
4
web/kvm/navbar-gpio.pug
Normal file
@ -0,0 +1,4 @@
|
||||
li(id="gpio-dropdown" class="right feature-disabled")
|
||||
a(class="menu-button" id="gpio-menu-button" href="#")
|
||||
| GPIO ↴
|
||||
div(data-dont-hide-menu id="gpio-menu" class="menu")
|
||||
@ -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
|
||||
|
||||
@ -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
163
web/share/js/kvm/gpio.js
Normal 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} ↴`;
|
||||
}
|
||||
|
||||
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> </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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user