diff options
-rw-r--r-- | PKGBUILD | 1 | ||||
-rw-r--r-- | configs/kvmd/main/v3-hdmi-rpi4.yaml | 4 | ||||
-rw-r--r-- | kvmd/apps/__init__.py | 5 | ||||
-rw-r--r-- | kvmd/apps/kvmd/info/__init__.py | 2 | ||||
-rw-r--r-- | kvmd/apps/kvmd/info/fan.py | 98 | ||||
-rw-r--r-- | web/kvm/index.html | 38 | ||||
-rw-r--r-- | web/kvm/navbar-health.pug | 13 | ||||
-rw-r--r-- | web/kvm/window-about.pug | 2 | ||||
-rw-r--r-- | web/share/js/kvm/session.js | 133 |
9 files changed, 258 insertions, 38 deletions
@@ -200,6 +200,7 @@ for _variant in "${_variants[@]}"; do if [ -f configs/kvmd/fan/$_platform.ini ]; then backup=(\"\${backup[@]}\" etc/kvmd/fan.ini) + depends=(\"\${depends[@]}\" \"kvmd-fan>=0.18\") install -DTm444 configs/kvmd/fan/$_platform.ini \"\$pkgdir/etc/kvmd/fan.ini\" fi diff --git a/configs/kvmd/main/v3-hdmi-rpi4.yaml b/configs/kvmd/main/v3-hdmi-rpi4.yaml index 6d113d7c..7de62ccd 100644 --- a/configs/kvmd/main/v3-hdmi-rpi4.yaml +++ b/configs/kvmd/main/v3-hdmi-rpi4.yaml @@ -12,6 +12,10 @@ kvmd: auth: !include auth.yaml + info: + fan: + unix: /run/kvmd/fan.sock + hid: type: otg keyboard: diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py index b31ac936..75600390 100644 --- a/kvmd/apps/__init__.py +++ b/kvmd/apps/__init__.py @@ -375,6 +375,11 @@ def _get_config_scheme() -> Dict: "vcgencmd_cmd": Option(["/opt/vc/bin/vcgencmd"], type=valid_command), "state_poll": Option(10.0, type=valid_float_f01), }, + "fan": { + "unix": Option("", type=valid_abs_path, if_empty="", unpack_as="unix_path"), + "timeout": Option(5.0, type=valid_float_f01), + "state_poll": Option(5.0, type=valid_float_f01), + }, }, "hid": { diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py index 458eec52..53a02f88 100644 --- a/kvmd/apps/kvmd/info/__init__.py +++ b/kvmd/apps/kvmd/info/__init__.py @@ -29,6 +29,7 @@ from .system import SystemInfoSubmanager from .meta import MetaInfoSubmanager from .extras import ExtrasInfoSubmanager from .hw import HwInfoSubmanager +from .fan import FanInfoSubmanager # ===== @@ -39,6 +40,7 @@ class InfoManager: "meta": MetaInfoSubmanager(config.kvmd.info.meta), "extras": ExtrasInfoSubmanager(config), "hw": HwInfoSubmanager(**config.kvmd.info.hw._unpack()), + "fan": FanInfoSubmanager(**config.kvmd.info.fan._unpack()), } def get_subs(self) -> Set[str]: diff --git a/kvmd/apps/kvmd/info/fan.py b/kvmd/apps/kvmd/info/fan.py new file mode 100644 index 00000000..d4529150 --- /dev/null +++ b/kvmd/apps/kvmd/info/fan.py @@ -0,0 +1,98 @@ +# ========================================================================== # +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2022 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/>. # +# # +# ========================================================================== # + + +import copy +import asyncio + +from typing import Dict +from typing import AsyncGenerator +from typing import Optional + +import aiohttp + +from ....logging import get_logger + +from .... import aiotools +from .... import htclient + +from .base import BaseInfoSubmanager + + +# ===== +class FanInfoSubmanager(BaseInfoSubmanager): + def __init__( + self, + unix_path: str, + timeout: float, + state_poll: float, + ) -> None: + + self.__unix_path = unix_path + self.__timeout = timeout + self.__state_poll = state_poll + + async def get_state(self) -> Dict: + return { + "monitored": bool(self.__unix_path), + "state": ((await self.__get_fan_state() if self.__unix_path else None)), + } + + async def poll_state(self) -> AsyncGenerator[Dict, None]: + prev_state: Dict = {} + while True: + if self.__unix_path: + pure = state = await self.get_state() + if pure["state"] is not None: + try: + pure = copy.deepcopy(state) + pure["state"]["service"]["now_ts"] = 0 + except Exception: + pass + if pure != prev_state: + yield state + prev_state = pure + await asyncio.sleep(self.__state_poll) + else: + yield (await self.get_state()) + await aiotools.wait_infinite() + + # ===== + + async def __get_fan_state(self) -> Optional[Dict]: + try: + async with self.__make_http_session() as session: + async with session.get("http://localhost/state") as response: + htclient.raise_not_200(response) + return (await response.json())["result"] + except Exception as err: + get_logger(0).error("Can't read fan state: %s", err) + return None + + def __make_http_session(self) -> aiohttp.ClientSession: + kwargs: Dict = { + "headers": { + "User-Agent": htclient.make_user_agent("KVMD"), + }, + "timeout": aiohttp.ClientTimeout(total=self.__timeout), + "connector": aiohttp.UnixConnector(path=self.__unix_path) + } + return aiohttp.ClientSession(**kwargs) diff --git a/web/kvm/index.html b/web/kvm/index.html index 1000ddaa..1007bb99 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -106,6 +106,38 @@ </div> </li> </div> + <div class="hidden" id="fan-health-dropdown"> + <li class="left"><a class="menu-button" href="#"><img class="hidden" data-dont-hide-menu id="fan-health-led" src="/share/svg/led-fan.svg"></a> + <div class="menu" data-dont-hide-menu> + <div class="text"> + <table> + <tr> + <td rowspan="2"><img class="sign " src="/share/svg/warning.svg"></td> + <td style="line-height:1.5"><b>Raspberry Pi's health is at risk</b></td> + </tr> + <tr> + <td><sup style="line-height:1">This is not a drill! A red icon indicates a current issue,<br> + a yellow one that was observed in the past</sup></td> + </tr> + </table> + </div> + <div id="fan-health-message-fail"> + <hr> + <div class="text"> + <table> + <tr> + <td rowspan="2"><img class="sign led-gray" src="/share/svg/led-fan.svg"></td> + <td style="line-height:1.5"><b>Fan failed</b></td> + </tr> + <tr> + <td><sup style="line-height:1">A fan error occured, please check the log</sup></td> + </tr> + </table> + </div> + </div> + </div> + </li> + </div> <li class="right"><a class="menu-button" href="#"><img class="led-gray" data-dont-hide-menu id="link-led" src="/share/svg/led-link.svg"><img class="led-gray" data-dont-hide-menu id="stream-led" src="/share/svg/led-stream.svg"><img class="led-gray" data-dont-hide-menu id="hid-keyboard-led" src="/share/svg/led-hid-keyboard.svg"><img class="led-gray" data-dont-hide-menu id="hid-mouse-led" src="/share/svg/led-hid-mouse.svg">System</a> <div class="menu" data-dont-hide-menu> <table class="kv" style="width: calc(100% - 20px)"> @@ -1539,10 +1571,10 @@ <div class="code" id="about-meta"><span class="code-comment">No data</span> </div> </div> - <input type="radio" name="about-tab-button" id="about-tab-hw-button"> - <label for="about-tab-hw-button">Hardware</label> + <input type="radio" name="about-tab-button" id="about-tab-hardware-button"> + <label for="about-tab-hardware-button">Hardware</label> <div class="tab"> - <div class="code" id="about-hw"><span class="code-comment">No data</span> + <div class="code" id="about-hardware"><span class="code-comment">No data</span> </div> </div> <input type="radio" name="about-tab-button" id="about-tab-version-button"> diff --git a/web/kvm/navbar-health.pug b/web/kvm/navbar-health.pug index 91003bab..455f0339 100644 --- a/web/kvm/navbar-health.pug +++ b/web/kvm/navbar-health.pug @@ -17,3 +17,16 @@ div(id="hw-health-dropdown" class="hidden") +menu_message("led-overheating", "Overheating detected", "led-gray") | Frequency capping due to overheating,#[br] | improve cooling of the Raspberry Pi + +div(id="fan-health-dropdown" class="hidden") + li(class="left") + a(class="menu-button" href="#") + +navbar_led("fan-health-led", "led-fan", "hidden") + div(data-dont-hide-menu class="menu") + +menu_message("warning", "Raspberry Pi's health is at risk") + | This is not a drill! A red icon indicates a current issue,#[br] + | a yellow one that was observed in the past + div(id="fan-health-message-fail") + hr + +menu_message("led-fan", "Fan failed", "led-gray") + | A fan error occured, please check the log diff --git a/web/kvm/window-about.pug b/web/kvm/window-about.pug index 830ddb10..18b5f23c 100644 --- a/web/kvm/window-about.pug +++ b/web/kvm/window-about.pug @@ -28,7 +28,7 @@ div(id="about-window" class="window") br div(class="tabs-box") +about_tab("meta", "Meta", true) - +about_tab("hw", "Hardware") + +about_tab("hardware", "Hardware") +about_tab("version", "Version") +about_tab("thanks", "Thanks") diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js index 68320655..72d75ab6 100644 --- a/web/share/js/kvm/session.js +++ b/web/share/js/kvm/session.js @@ -53,6 +53,9 @@ export function Session() { var __gpio = new Gpio(__recorder); var __ocr = new Ocr(__streamer.getGeometry); + var __info_hw_state = null; + var __info_fan_state = null; + var __init__ = function() { __startSession(); }; @@ -86,16 +89,6 @@ export function Session() { }; var __setAboutInfoHw = function(state) { - $("about-hw").innerHTML = ` - Platform base: <span class="code-comment">${state.platform.base}</span><br> - <hr> - Temperature: - ${__formatTemp(state.health.temp)} - <hr> - Throttling: - ${__formatThrottling(state.health.throttling)} - `; - if (state.health.throttling !== null) { let flags = state.health.throttling.parsed_flags; let undervoltage = (flags.undervoltage.now || flags.undervoltage.past); @@ -107,31 +100,74 @@ export function Session() { tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage); tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped); } + __info_hw_state = state; + __renderAboutInfoHardware(); }; - var __setExtras = 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; - show_hook = function() { - tools.info("Terminal opened: ", path); - $("webterm-iframe").src = path; - }; - close_hook = function() { - tools.info("Terminal closed"); - $("webterm-iframe").src = ""; - }; + var __setAboutInfoFan = 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.feature.setEnabled($("webterm"), has_webterm); - $("webterm-window").show_hook = show_hook; - $("webterm-window").close_hook = close_hook; + tools.hidden.setVisible($("fan-health-dropdown"), failed); + $("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden"); - __streamer.setJanusEnabled( - (state.janus && (state.janus.enabled || state.janus.started)) - || (state.janus_static && (state.janus_static.enabled || state.janus_static.started)) - ); + __info_fan_state = state; + __renderAboutInfoHardware(); + }; + + var __renderAboutInfoHardware = function() { + let html = ""; + if (__info_hw_state !== null) { + html += ` + Platform base: <span class="code-comment">${__info_hw_state.platform.base}</span><br> + <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 __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) { @@ -158,13 +194,16 @@ export function Session() { }; var __formatThrottleError = function(flags) { - let colored = ((color, text) => `<font color="${color}">${text}</font>`); return ` - ${flags["now"] ? colored("red", "RIGHT NOW") : colored("green", "No")}; - ${flags["past"] ? colored("red", "In the past") : colored("green", "Never")} + ${flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No")}; + ${flags["past"] ? __colored("red", "In the past") : __colored("green", "Never")} `; }; + var __colored = function(color, text) { + return `<font color="${color}">${text}</font>`; + }; + var __setAboutInfoSystem = function(state) { $("about-version").innerHTML = ` KVMD: <span class="code-comment">${state.kvmd.version}</span><br> @@ -203,6 +242,31 @@ export function Session() { return text + "</ul>"; }; + var __setExtras = 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; + 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($("webterm"), has_webterm); + $("webterm-window").show_hook = show_hook; + $("webterm-window").close_hook = close_hook; + + __streamer.setJanusEnabled( + (state.janus && (state.janus.enabled || state.janus.started)) + || (state.janus_static && (state.janus_static.enabled || state.janus_static.started)) + ); + }; + var __startSession = function() { $("link-led").className = "led-yellow"; $("link-led").title = "Connecting..."; @@ -244,6 +308,7 @@ export function Session() { case "pong": __missed_heartbeats = 0; break; case "info_meta_state": __setAboutInfoMeta(data.event); break; case "info_hw_state": __setAboutInfoHw(data.event); break; + case "info_fan_state": __setAboutInfoFan(data.event); break; case "info_system_state": __setAboutInfoSystem(data.event); break; case "info_extras_state": __setExtras(data.event); break; case "gpio_model_state": __gpio.setModel(data.event); break; |