From 0d35fb76e0b9738a22e0631b5e5fb96287711e40 Mon Sep 17 00:00:00 2001 From: rawhide kobayashi Date: Wed, 12 Mar 2025 00:18:27 -0500 Subject: [PATCH] huuuuge organizational changes, added profile editing via webui, added hwinfo monitoring in webui, config changes work live --- profiles/hong.toml => hong.toml | 0 profiles/default.toml | 28 ++ serialtest.py | 28 -- webui/app.py | 62 ----- webui/graphtest.py | 74 ----- webui/ipkvm/__init__.py | 148 +--------- webui/ipkvm/app.py | 8 + webui/ipkvm/events.py | 103 ------- webui/ipkvm/feed.py | 92 ------ webui/ipkvm/hwinfo.py | 77 ----- webui/ipkvm/routes.py | 4 +- webui/ipkvm/states/__init__.py | 0 webui/ipkvm/states/events.py | 38 +++ webui/ipkvm/{ => states}/states.py | 9 +- webui/ipkvm/static/Bsodwindows10.png | Bin 0 -> 38675 bytes webui/ipkvm/static/css/style.css | 4 + webui/ipkvm/static/js/my-codemirror.js | 4 +- webui/ipkvm/static/js/profiles.js | 97 +++++++ webui/ipkvm/static/js/table.js | 2 + webui/ipkvm/templates/default.toml | 28 ++ webui/ipkvm/templates/index.html | 33 ++- webui/ipkvm/util/__init__.py | 4 +- webui/ipkvm/util/graphs/__init__.py | 0 webui/ipkvm/util/{ => graphs}/graphs.py | 13 +- webui/ipkvm/util/hwinfo/__init__.py | 3 + webui/ipkvm/util/hwinfo/hwinfo.py | 81 ++++++ webui/ipkvm/util/mkb/__init__.py | 3 + webui/ipkvm/util/mkb/events.py | 50 ++++ webui/ipkvm/util/mkb/mkb.py | 110 ++++++++ .../ipkvm/util/{mkb.py => mkb/post_codes.py} | 255 ----------------- webui/ipkvm/util/mkb/scancodes.py | 157 +++++++++++ webui/ipkvm/util/profiles/__init__.py | 3 + webui/ipkvm/util/profiles/events.py | 15 + webui/ipkvm/util/profiles/profiles.py | 96 +++++++ webui/ipkvm/util/video.py | 139 --------- webui/ipkvm/util/video/__init__.py | 3 + webui/ipkvm/util/video/video.py | 263 ++++++++++++++++++ webui/launch.py | 5 +- webui/serial_test.py | 70 ----- webui/state_test.py | 47 ---- 40 files changed, 1025 insertions(+), 1131 deletions(-) rename profiles/hong.toml => hong.toml (100%) create mode 100644 profiles/default.toml delete mode 100644 serialtest.py delete mode 100644 webui/app.py delete mode 100644 webui/graphtest.py create mode 100644 webui/ipkvm/app.py delete mode 100644 webui/ipkvm/events.py delete mode 100644 webui/ipkvm/feed.py delete mode 100644 webui/ipkvm/hwinfo.py create mode 100644 webui/ipkvm/states/__init__.py create mode 100644 webui/ipkvm/states/events.py rename webui/ipkvm/{ => states}/states.py (95%) create mode 100755 webui/ipkvm/static/Bsodwindows10.png create mode 100644 webui/ipkvm/static/js/profiles.js create mode 100644 webui/ipkvm/templates/default.toml create mode 100644 webui/ipkvm/util/graphs/__init__.py rename webui/ipkvm/util/{ => graphs}/graphs.py (85%) create mode 100644 webui/ipkvm/util/hwinfo/__init__.py create mode 100644 webui/ipkvm/util/hwinfo/hwinfo.py create mode 100644 webui/ipkvm/util/mkb/__init__.py create mode 100644 webui/ipkvm/util/mkb/events.py create mode 100644 webui/ipkvm/util/mkb/mkb.py rename webui/ipkvm/util/{mkb.py => mkb/post_codes.py} (74%) create mode 100644 webui/ipkvm/util/mkb/scancodes.py create mode 100644 webui/ipkvm/util/profiles/__init__.py create mode 100644 webui/ipkvm/util/profiles/events.py create mode 100644 webui/ipkvm/util/profiles/profiles.py delete mode 100644 webui/ipkvm/util/video.py create mode 100644 webui/ipkvm/util/video/__init__.py create mode 100644 webui/ipkvm/util/video/video.py delete mode 100644 webui/serial_test.py delete mode 100644 webui/state_test.py diff --git a/profiles/hong.toml b/hong.toml similarity index 100% rename from profiles/hong.toml rename to hong.toml diff --git a/profiles/default.toml b/profiles/default.toml new file mode 100644 index 0000000..465c70b --- /dev/null +++ b/profiles/default.toml @@ -0,0 +1,28 @@ +# This is a text editor! Type in here! +[server] +esp32_serial = "usb-1a86_USB_Single_Serial_585D015807-if00" + +[server.video_device] +friendly_name = "WARRKY USB 3.0" +resolution = "1920x1080" +fps = "60.000" + +[client] +hostname = "10.20.30.48" +hwinfo_port = "60000" # Unless you've changed it! + +[client.overclocking.common] # Just some ideas of how the settings may work for your system +# "ACPI SRAT L3 Cache As NUMA Domain" = "Enabled" +# "Spread Spectrum" = "Disabled" +# "PBO Limits" = "Motherboard" +# "Precision Boost Overdrive Scalar" = "10X" +# "Max CPU Boost Clock Override(+)" = "200" +# "Auto Driver Installer" = "Disabled" +# "dGPU Only Mode" = "Enabled" + +[client.overclocking.cpu] + +[client.overclocking.memory] +# "DRAM Profile Setting" = "XMP1-6000" +# "DRAM Performance Mode" = "Aggressive" +# "SOC/Uncore OC Voltage (VDD_SOC)" = "1.1" \ No newline at end of file diff --git a/serialtest.py b/serialtest.py deleted file mode 100644 index f60f512..0000000 --- a/serialtest.py +++ /dev/null @@ -1,28 +0,0 @@ -import serial -import sys -import datetime - -def read_serial(port): - try: - # Open the serial port - with serial.Serial(port, 115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) as ser: - print(f"Listening on {port} at 115200 baud...") - while True: - # Read a line from the serial port - line = ser.read() - # Print the raw data - print(f'{datetime.datetime.now()} {line}') - except serial.SerialException as e: - print(f"Error: {e}") - except KeyboardInterrupt: - print("Exiting...") - sys.exit() - -if __name__ == "__main__": - #if len(sys.argv) != 2: - # print("Usage: python read_serial.py ") - # print("Example: python read_serial.py COM3 (Windows) or /dev/ttyUSB0 (Linux)") - # sys.exit(1) - - #port_name = sys.argv[1] - read_serial("/dev/serial/by-id/usb-1a86_USB_Single_Serial_585D015807-if00") diff --git a/webui/app.py b/webui/app.py deleted file mode 100644 index b56f6a3..0000000 --- a/webui/app.py +++ /dev/null @@ -1,62 +0,0 @@ -import subprocess -from flask import Flask, Response - -app = Flask(__name__) - -def generate(): - # FFmpeg command to capture the MJPEG stream without re-encoding. - command = [ - 'ffmpeg', - '-f', 'v4l2', - '-input_format', 'mjpeg', '-video_size', '1920x1080', '-framerate', '60.00', - '-i', '/dev/video0', - '-c', 'copy', - '-f', 'mjpeg', - 'pipe:1' - ] - # Start the FFmpeg subprocess. - process = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=10**8) - data = b"" - while True: - # Read raw bytes from FFmpeg's stdout. - chunk = process.stdout.read(1024) - if not chunk: - break - data += chunk - - # Look for complete JPEG frames by finding start and end markers. - while True: - start = data.find(b'\xff\xd8') # JPEG start - end = data.find(b'\xff\xd9') # JPEG end - if start != -1 and end != -1 and end > start: - # Extract the JPEG frame. - jpg = data[start:end+2] - data = data[end+2:] - # Yield the frame with the required multipart MJPEG boundaries. - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + jpg + b'\r\n') - else: - break - -@app.route('/video_feed') -def video_feed(): - # Set the MIME type to multipart so browsers render it as an MJPEG stream. - return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame') - -@app.route('/') -def index(): - return """ - - - Webcam Stream - - -

Webcam Stream

- - - - """ - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) - diff --git a/webui/graphtest.py b/webui/graphtest.py deleted file mode 100644 index f7fd086..0000000 --- a/webui/graphtest.py +++ /dev/null @@ -1,74 +0,0 @@ -import networkx as nx -import serial -#from ipkvm.util.mkb import HIDKeyCode -import json -import time - -# Load the Graphviz file -graph = nx.nx_agraph.read_dot("bios-maps/asrock/b650e-riptide-wifi.gv") - -print(type(graph)) - -print(graph) - -print(graph.edges()) - -# Example: Access node attributes -for node, data in graph.nodes(data=True): - print(f"Node: {node}, Attributes: {data}") - -print(graph.edges(data=True)) - -# Example: Access edge attributes (keypress actions) -for edge_a, edge_b, data in graph.edges(data=True): - print(f"Edge: {edge_a} to {edge_b}, Attributes: {data}") - -path = nx.shortest_path(graph, "Exit", "TDP to 105W") - -for pair in nx.utils.pairwise(path): - print(pair) - print(graph.edges(pair, data=True)) - -edge_path = list(zip(path[:-1], path[1:])) - -print("Node path:", path) -print("Edge path:", edge_path) - -edge_path_with_data = [(u, v, graph[u][v]) for u, v in edge_path] -print("Edge path with data:", edge_path_with_data) - -print("GENERATOR TEST") - -for path in sorted(nx.all_simple_edge_paths(graph, "Exit", "Tool")): - for edge in path: - #print(edge) - keys = graph.get_edge_data(edge[0], edge[1])[0]["keypath"].split(',') - #print(keys) - - # with serial.Serial('/dev/serial/by-id/usb-1a86_USB_Single_Serial_585D015807-if00', 115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) as ser: - for key in keys: - print(key) - #test_json_a = { - # "mouseX": 99999, - # "mouseY": 99999, - # "mouse_down": ["rbutton", "lbutton"], - # "mouse_up": ["otherbutton"], - # "key_down": [HIDKeyCode[key]], - # "key_up": [] - #} -# # - #print(key) - ##ser.write(json.dumps(test_json_a).encode()) -# - #test_json_a = { - # "mouseX": 99999, - # "mouseY": 99999, - # "mouse_down": ["rbutton", "lbutton"], - # "mouse_up": ["otherbutton"], - # "key_down": [], - # "key_up": [HIDKeyCode[key]] - #} -# # - #print(key) - ##ser.write(json.dumps(test_json_a).encode()) - ##time.sleep(0.1) \ No newline at end of file diff --git a/webui/ipkvm/__init__.py b/webui/ipkvm/__init__.py index dc4f469..155057f 100644 --- a/webui/ipkvm/__init__.py +++ b/webui/ipkvm/__init__.py @@ -1,147 +1,3 @@ -from os import name, listdir -from flask import Flask -from flask_socketio import SocketIO -import tomlkit -import logging +from ipkvm import routes - -app = Flask(__name__) -ui = SocketIO(app) -logger = app.logger -logger.setLevel(logging.INFO) - -from ipkvm.util import video - -def new_profile(): - profile = tomlkit.document() - server = tomlkit.table() - video_device = tomlkit.table() - client = tomlkit.table() - - device_list = video.create_device_list() - print(f"Detected {len(device_list)} video devices on your system.") - print("Please enter the number of your preferred video device.") - for i, device in enumerate(device_list): - print(f"{i + 1}. {device.friendly_name}") - - device = int(input("> ")) - 1 - video_device["friendly_name"] = device_list[device].friendly_name - - if len(device_list[device].video_formats) > 1: - print("Please enter your preferred video input format: ") - for i, format in enumerate(device_list[device].video_formats): - print(f"{i + 1}. {format}") - - format = list(device_list[device].video_formats.keys())[int(input("> ")) - 1] - video_device["format"] = format - - else: - format = next(iter(device_list[device].video_formats)) - print(f"Video input format auto-detected as {format}!") - video_device["format"] = format - - print("Please enter the number of your preferred video resolution.") - - for i, resolution in enumerate(device_list[device].video_formats[format]): - print(f"{i + 1}. {resolution}") - - resolution = list(device_list[device].video_formats[format].keys())[int(input("> ")) - 1] - video_device["resolution"] = resolution - - print("Please enter the number of your preferred video refresh rate.") - - for i, fps in enumerate(device_list[device].video_formats[format][resolution]): - print(f"{i + 1}. {fps}") - - fps = str(device_list[device].video_formats[format][resolution][int(input("> ")) - 1]) - video_device["fps"] = fps - - if name == "posix": - serial_devices = listdir("/dev/serial/by-id/") - - else: - serial_devices = [] - - if len(serial_devices) > 1: - print("Please enter the number of your preferred ESP32 serial device.") - for i, serial_device in enumerate(serial_devices): - print(f"{i + 1}. {serial_device}") - - server["esp32_serial"] = serial_devices[int(input("> ")) - 1] - - elif len(serial_devices) == 1: - print(f"ESP32 auto-detected as {serial_devices[0]}!") - server["esp32_serial"] = serial_devices[0] - - else: - raise RuntimeError("No valid ESP32 devices connected!") - - print("Please enter the hostname or IP address of your client.") - client["hostname"] = input("> ") - - print("Please enter the port for RemoteHWInfo, if you have changed it from the default [60000].") - port = input("> ") - - if port == "": - port = "60000" - - client["hwinfo_port"] = port - - print("Please enter your new profile name.") - profile_name = input("> ") - - server["video_device"] = video_device - client["overclocking"] = { - "common": {}, - "cpu": {}, - "memory": {} - } - profile["server"] = server - profile["client"] = client - - #profile: dict[str, dict[str, str | dict[str, str]] | dict[str, str | dict[str, dict[str, str]]]] = { - # "server": { - # "esp32_serial": serial_device, - # "video_device": { - # "friendly_name": device_list[device].friendly_name, - # "format": format, - # "resolution": resolution, - # "fps": fps - # } - # }, -# - # "client": { - # "hostname": hostname, - # "hwinfo_port": port, -# - # "overclocking": { - # "common": {}, - # "cpu": {}, - # "memory": {} - # } - # } - #} - - with open(f"profiles/{profile_name}.toml", 'w') as file: - tomlkit.dump(profile, file) - - return profile - -if len(listdir("profiles")) == 0: - print("No profiles found, entering runtime profile configuration...") - profile = new_profile() - -elif len(listdir("profiles")) == 1: - print(f"Only one profile found, autoloading {listdir("profiles")[0]}...") - with open(f"profiles/{listdir("profiles")[0]}", 'r') as file: - profile = tomlkit.load(file) - -from ipkvm import feed -from ipkvm.util.mkb import Esp32Serial -from ipkvm.hwinfo import HWInfoMonitor - -frame_buffer = feed.FrameBuffer() -esp32_serial = Esp32Serial() -monitor = HWInfoMonitor() - -from ipkvm import routes, events +__all__ = ["routes"] diff --git a/webui/ipkvm/app.py b/webui/ipkvm/app.py new file mode 100644 index 0000000..a691bf4 --- /dev/null +++ b/webui/ipkvm/app.py @@ -0,0 +1,8 @@ +from flask import Flask +from flask_socketio import SocketIO +import logging + +app = Flask(__name__) +ui = SocketIO(app) +logger = app.logger +logger.setLevel(logging.INFO) \ No newline at end of file diff --git a/webui/ipkvm/events.py b/webui/ipkvm/events.py deleted file mode 100644 index 372120b..0000000 --- a/webui/ipkvm/events.py +++ /dev/null @@ -1,103 +0,0 @@ -from ipkvm import ui -from ipkvm import esp32_serial -from ipkvm.util.mkb import HIDKeyCode, HIDMouseScanCodes, GPIO -import time -from ipkvm.util import graphs -from ipkvm import states -from ipkvm import profile -import tomlkit - -def power_switch(delay: float): - msg = { - "pwr": GPIO.HIGH.value - } - esp32_serial.mkb_queue.put(msg) - time.sleep(delay) - msg = { - "pwr": GPIO.LOW.value - } - esp32_serial.mkb_queue.put(msg) - -@ui.on("power_on") -def handle_poweron(): - states.model.power_on() - -@ui.on("soft_power_off") -def handle_soft_poweroff(): - states.model.soft_shutdown() - -@ui.on("hard_power_off") -def handle_hard_poweroff(): - states.model.hard_shutdown() - -@ui.on("reboot_into_bios") -def handle_reboot_bios(): - states.model.reboot_into_bios() - -@ui.on("clear_cmos") -def handle_clear_cmos(): - msg = { - "cmos": GPIO.HIGH.value - } - esp32_serial.mkb_queue.put(msg) - time.sleep(0.2) - msg = { - "cmos": GPIO.LOW.value - } - esp32_serial.mkb_queue.put(msg) - - time.sleep(1) - - power_switch(0.2) - spam_delete_until_bios() - -@ui.on('key_down') -def handle_keydown(data: str): - msg = { - "key_down": HIDKeyCode[data].value - } - - esp32_serial.mkb_queue.put(msg) - -@ui.on('key_up') -def handle_keyup(data: str): - msg = { - "key_up": HIDKeyCode[data].value - } - - esp32_serial.mkb_queue.put(msg) - -@ui.on("mouse_move") -def handle_mousemove(data: list[int]): - msg = { - "mouse_coord": { - "x": data[0], - "y": data[1] - } - } - - esp32_serial.mkb_queue.put(msg) - -@ui.on('mouse_down') -def handle_mousedown(data: int): - msg = { - "mouse_down": HIDMouseScanCodes[data] - } - - esp32_serial.mkb_queue.put(msg) - -@ui.on('mouse_up') -def handle_mouseup(data: int): - msg = { - "mouse_up": HIDMouseScanCodes[data] - } - - esp32_serial.mkb_queue.put(msg) - -@ui.on("test_route") -def handle_test_route(): - graphs.test_route() - -@ui.on("get_current_profile") -def handle_current_profile(): - return tomlkit.dumps(profile) \ No newline at end of file diff --git a/webui/ipkvm/feed.py b/webui/ipkvm/feed.py deleted file mode 100644 index d3b4e10..0000000 --- a/webui/ipkvm/feed.py +++ /dev/null @@ -1,92 +0,0 @@ -from os import name -import threading -import av, av.container -import cv2 -from ipkvm import profile -from ipkvm.util import video -from ipkvm import logger -import time -from PIL import Image -import io - -class FrameBuffer(threading.Thread): - def __init__(self): - super().__init__() - self.buffer_lock = threading.Lock() - self.cur_frame = None - self.new_frame = threading.Event() - self.start() - - def run(self): - self.capture_feed() - - - def capture_feed(self): - device = self.acquire_device() - while True: - # try: - # for frame in device.decode(video=0): - # frame = frame.to_ndarray(format='rgb24') - # ret, self.cur_frame = cv2.imencode('.jpg', frame) - # print(ret) - # cv2.imwrite("test.jpg", frame) - # except av.BlockingIOError: - # pass - - success, frame = device.read() - if not success: - break - else: - # ret, buffer = cv2.imencode('.jpg', frame) - # self.cur_frame = buffer.tobytes() - # Convert BGR (OpenCV) to RGB (PIL) - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Convert to PIL Image - img = Image.fromarray(frame_rgb) - - # Save to a bytes buffer (for in-memory use) - buffer = io.BytesIO() - img.save(buffer, format="JPEG") - jpeg_bytes = buffer.getvalue() # This contains the JPEG image as bytes - self.cur_frame = jpeg_bytes - - self.new_frame.set() - - - # def acquire_device(self): - # device_list = video.create_device_list() - # device_path = "" - # for device in device_list: - # if device.friendly_name == profile["video_device"]["friendly_name"]: - # device_path = device.path -# - # if name == "posix": - # return av.open(device_path, format="video4linux2", container_options={ - # "framerate": profile["video_device"]["fps"], - # "video_size": profile["video_device"]["resolution"], - # "input_format": profile["video_device"]["format"] - # }) -# - # else: - # raise RuntimeError("We're on something other than Linux, and that's not yet supported!") - - def acquire_device(self): - device_list = video.create_device_list() - device_path = "" - for device in device_list: - if device.friendly_name == profile["server"]["video_device"]["friendly_name"]: - device_path = device.path - - if name == "posix": - device = cv2.VideoCapture(device_path) # Use default webcam (index 0) - - else: - raise RuntimeError("We're on something other than Linux, and that's not yet supported!") - - device.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"YUYV")) - device.set(cv2.CAP_PROP_FRAME_WIDTH, int(profile["server"]["video_device"]["resolution"].split('x')[0])) - device.set(cv2.CAP_PROP_FRAME_HEIGHT, int(profile["server"]["video_device"]["resolution"].split('x')[1])) - device.set(cv2.CAP_PROP_FPS, float(profile["server"]["video_device"]["fps"])) - - return device \ No newline at end of file diff --git a/webui/ipkvm/hwinfo.py b/webui/ipkvm/hwinfo.py deleted file mode 100644 index 3a60718..0000000 --- a/webui/ipkvm/hwinfo.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import threading -import re -import requests -#from ipkvm import profile -import time -from rich import print -import pandas as pd -from ipkvm import ui - -class HWInfoMonitor(threading.Thread): - def __init__(self): - super().__init__() - #self.request_url = f"http://{profile["client"]["hostname"]}:{profile["client"]["port"]}/json.json" - self.request_url = "http://10.20.30.48:60000/json.json" - self.dataframe = self.create_dataframe() - self.start() - - def run(self): - while True: - self.create_dataframe() - time.sleep(0.25) - - def create_dataframe(self): - while not self.is_hwinfo_alive(): - time.sleep(1) - - request = requests.get(self.request_url) - - data = request.json() - - cpu_list: list[str] = [] - vid_list: list[str] = [] - mhz_list: list[str] = [] - ccd_list: list[str] = [] - temp_list: list[str] = [] - power_list: list[str] = [] - - for reading in data["hwinfo"]["readings"]: - label = reading["labelOriginal"] - - match = re.match(r"(?PCore[0-9]* \(CCD[0-9]\))|(?PCore [0-9]* VID)|(?PCore [0-9]* T0 Effective Clock)|(?PCore [0-9]* Power)", label) - - if match: - if match.group("core_ccd"): - core_ccd = match.group("core_ccd").split(' ') - core_ccd[0] = core_ccd[0][:4] + ' ' + core_ccd[0][4:] - cpu_list.append(core_ccd[0]) - ccd_list.append(core_ccd[1].strip('()')) - temp_list.append(round(reading["value"], 2)) - - elif match.group("core_vid"): - vid_list.append(reading["value"]) - - elif match.group("core_mhz"): - mhz_list.append(round(reading["value"], 2)) - - elif match.group("core_power"): - power_list.append(round(reading["value"], 2)) - - core_dataframe = pd.DataFrame({ - "CCD": ccd_list, - "Clk": mhz_list, - "VID": vid_list, - "Power": power_list, - "Temp": temp_list - }, index=cpu_list) - - ui.emit("update_core_info_table", core_dataframe.to_dict("index")) - - def is_hwinfo_alive(self): - request = requests.get(self.request_url) - - if request.status_code == 200 and len(request.json()) > 0: - return True - - return False diff --git a/webui/ipkvm/routes.py b/webui/ipkvm/routes.py index 5b4a24d..7b5ade6 100644 --- a/webui/ipkvm/routes.py +++ b/webui/ipkvm/routes.py @@ -1,5 +1,5 @@ -from ipkvm import app -from ipkvm import frame_buffer +from ipkvm.app import app +from ipkvm.util.video import frame_buffer from flask import Response, render_template diff --git a/webui/ipkvm/states/__init__.py b/webui/ipkvm/states/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webui/ipkvm/states/events.py b/webui/ipkvm/states/events.py new file mode 100644 index 0000000..e3805ae --- /dev/null +++ b/webui/ipkvm/states/events.py @@ -0,0 +1,38 @@ +from ipkvm.app import ui + +@ui.on("power_on") +def handle_poweron(): + states.model.power_on() + +@ui.on("soft_power_off") +def handle_soft_poweroff(): + states.model.soft_shutdown() + +@ui.on("hard_power_off") +def handle_hard_poweroff(): + states.model.hard_shutdown() + +@ui.on("reboot_into_bios") +def handle_reboot_bios(): + states.model.reboot_into_bios() + +@ui.on("clear_cmos") +def handle_clear_cmos(): + msg = { + "cmos": GPIO.HIGH.value + } + esp32_serial.mkb_queue.put(msg) + time.sleep(0.2) + msg = { + "cmos": GPIO.LOW.value + } + esp32_serial.mkb_queue.put(msg) + + time.sleep(1) + + power_switch(0.2) + spam_delete_until_bios() + +@ui.on("test_route") +def handle_test_route(): + graphs.test_route() \ No newline at end of file diff --git a/webui/ipkvm/states.py b/webui/ipkvm/states/states.py similarity index 95% rename from webui/ipkvm/states.py rename to webui/ipkvm/states/states.py index b94b268..52aa3c4 100644 --- a/webui/ipkvm/states.py +++ b/webui/ipkvm/states/states.py @@ -2,12 +2,9 @@ from enum import Enum import time from transitions.experimental.utils import with_model_definitions, add_transitions, transition from transitions.extensions import GraphMachine -from ipkvm import esp32_serial +from ipkvm.util import esp32_serial from ipkvm.util.mkb import GPIO, HIDKeyCode -import logging -logging.basicConfig(level=logging.DEBUG) -# Set transitions' log level to INFO; DEBUG messages will be omitted -logging.getLogger('transitions').setLevel(logging.DEBUG) +from ipkvm.app import logging, ui class State(Enum): PoweredOff = "powered off" @@ -166,5 +163,3 @@ model = Overclocking() machine = MyMachine(model, states=State, initial=model.state) machine.get_graph().draw('my_state_diagram.svg', prog='dot') -print(model.client_powered) -print(model.state) \ No newline at end of file diff --git a/webui/ipkvm/static/Bsodwindows10.png b/webui/ipkvm/static/Bsodwindows10.png new file mode 100755 index 0000000000000000000000000000000000000000..8845ddffb21bfcc074cc20b1304e595c23ac0428 GIT binary patch literal 38675 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 literal 0 HcmV?d00001 diff --git a/webui/ipkvm/static/css/style.css b/webui/ipkvm/static/css/style.css index 5e7e7c2..3bbd2a5 100644 --- a/webui/ipkvm/static/css/style.css +++ b/webui/ipkvm/static/css/style.css @@ -68,4 +68,8 @@ bottom: 0; left: 0; overflow: auto; +} + +.first-option { + display:none; } \ No newline at end of file diff --git a/webui/ipkvm/static/js/my-codemirror.js b/webui/ipkvm/static/js/my-codemirror.js index aa0a25e..50ce9c8 100644 --- a/webui/ipkvm/static/js/my-codemirror.js +++ b/webui/ipkvm/static/js/my-codemirror.js @@ -1,8 +1,10 @@ +let code_mirror; + function codemirror_load() { const code_div = document.getElementById('codemirror'); - var code_mirror = CodeMirror(code_div, { + code_mirror = CodeMirror(code_div, { lineNumbers: true }) diff --git a/webui/ipkvm/static/js/profiles.js b/webui/ipkvm/static/js/profiles.js new file mode 100644 index 0000000..370591d --- /dev/null +++ b/webui/ipkvm/static/js/profiles.js @@ -0,0 +1,97 @@ +let profileDropdown; +let serialDropdown; +let videoDropdown; +let resolutionDropdown; +let fpsDropdown; +let deviceData = {}; + +function profiles_load() +{ + profileDropdown = document.getElementById("profile-select"); + serialDropdown = document.getElementById("serial-select"); + videoDropdown = document.getElementById("video-select"); + resolutionDropdown = document.getElementById("resolution-select"); + fpsDropdown = document.getElementById("fps-select"); + populate_video_devices(); + populate_serial_devices(); +} + +function update_toml(field, new_value) { + let re = new RegExp(String.raw`(${field}\s*=\s*)".*?"`, "g"); + return code_mirror.getValue().replace(re, `$1"${new_value}"`); +} + +function removeAllButFirstChild(element) { + while (element.children.length > 1) { + element.removeChild(element.lastChild); + } +} + +function populate_video_devices() +{ + removeAllButFirstChild(videoDropdown) + socket.emit("get_video_devices", (data) => { + deviceData = data; + Object.keys(data).forEach((key, index) => { + let option = document.createElement("option"); + option.id = key; + option.textContent = key; + videoDropdown.appendChild(option); + }); + }); +} + +function populate_resolution(deviceName) +{ + removeAllButFirstChild(resolutionDropdown) + Object.keys(deviceData[deviceName]["formats"]).forEach((key, index) => { + let option = document.createElement("option"); + option.id = key; + option.textContent = key; + resolutionDropdown.appendChild(option); + }); + + populate_fps(resolutionDropdown.value, videoDropdown.value); + + if (resolutionDropdown.value != "Resolution...") { + code_mirror.setValue(update_toml('resolution', resolutionDropdown.value)); + } +} + +function populate_fps(resolution, deviceName) +{ + removeAllButFirstChild(fpsDropdown) + deviceData[deviceName]["formats"][resolution].forEach((key, index) => { + let option = document.createElement("option"); + option.id = key; + option.textContent = key; + fpsDropdown.appendChild(option); + }); + + if (fpsDropdown.value != "FPS...") { + code_mirror.setValue(update_toml('fps', fpsDropdown.value)); + } +} + +function populate_serial_devices() +{ + removeAllButFirstChild(serialDropdown) + socket.emit("get_serial_devices", (data) => { + data.forEach((key, index) => { + let option = document.createElement("option"); + option.id = key; + option.textContent = key; + serialDropdown.appendChild(option); + }); + }); + + if (serialDropdown.value != "Serial device...") { + code_mirror.setValue(update_toml('esp32_serial', serialDropdown.value)); + } +} + +function save_new_profile() { + socket.emit("save_profile_as", code_mirror.getValue(), prompt("Input new profile name!")); +} + +window.addEventListener("load", profiles_load); \ No newline at end of file diff --git a/webui/ipkvm/static/js/table.js b/webui/ipkvm/static/js/table.js index 20e4271..1f7266e 100644 --- a/webui/ipkvm/static/js/table.js +++ b/webui/ipkvm/static/js/table.js @@ -9,6 +9,8 @@ function create_table(data) { let row_headers = [] let col_headers = [] + container.innerText = "" + Object.keys(data).forEach((key, index) => { row_headers.push(key) Object.keys(data[key]).forEach((value, subIndex) => { diff --git a/webui/ipkvm/templates/default.toml b/webui/ipkvm/templates/default.toml new file mode 100644 index 0000000..655a865 --- /dev/null +++ b/webui/ipkvm/templates/default.toml @@ -0,0 +1,28 @@ +# This is a text editor! Type in here! +[server] +esp32_serial = "Pick from above" + +[server.video_device] +friendly_name = "Pick from above" +resolution = "Pick from above" +fps = "Pick from above" + +[client] +hostname = "IP or hostname for your overclocking client" +hwinfo_port = "60000" # Unless you've changed it! + +[client.overclocking.common] # Just some ideas of how the settings may work for your system +# "ACPI SRAT L3 Cache As NUMA Domain" = "Enabled" +# "Spread Spectrum" = "Disabled" +# "PBO Limits" = "Motherboard" +# "Precision Boost Overdrive Scalar" = "10X" +# "Max CPU Boost Clock Override(+)" = "200" +# "Auto Driver Installer" = "Disabled" +# "dGPU Only Mode" = "Enabled" + +[client.overclocking.cpu] + +[client.overclocking.memory] +# "DRAM Profile Setting" = "XMP1-6000" +# "DRAM Performance Mode" = "Aggressive" +# "SOC/Uncore OC Voltage (VDD_SOC)" = "1.1" \ No newline at end of file diff --git a/webui/ipkvm/templates/index.html b/webui/ipkvm/templates/index.html index bdc82b7..e3f22b1 100644 --- a/webui/ipkvm/templates/index.html +++ b/webui/ipkvm/templates/index.html @@ -13,6 +13,7 @@ + @@ -24,26 +25,28 @@
-
+
Waiting for data...
diff --git a/webui/ipkvm/util/__init__.py b/webui/ipkvm/util/__init__.py index 8b13789..6d0bbee 100644 --- a/webui/ipkvm/util/__init__.py +++ b/webui/ipkvm/util/__init__.py @@ -1 +1,3 @@ - +from . import hwinfo +from .mkb import events +from .profiles import events diff --git a/webui/ipkvm/util/graphs/__init__.py b/webui/ipkvm/util/graphs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webui/ipkvm/util/graphs.py b/webui/ipkvm/util/graphs/graphs.py similarity index 85% rename from webui/ipkvm/util/graphs.py rename to webui/ipkvm/util/graphs/graphs.py index 7b7c984..e106e6d 100644 --- a/webui/ipkvm/util/graphs.py +++ b/webui/ipkvm/util/graphs/graphs.py @@ -1,21 +1,16 @@ import networkx as nx from networkx import Graph from typing import Any -from ipkvm import esp32_serial +from ipkvm.util import esp32_serial from ipkvm.util.mkb import ASCII2JS import time -import tomlkit # Type checker lunacy! type MultiDiGraph = Graph[Any] -visited_edges: list[set[str | int]] = [] - -with open("OC/test_oc_profile.toml") as file: - settings = tomlkit.parse(file.read()) key_delay = 0.1 -def traverse_path(graph: MultiDiGraph, node_a: str, node_b: str, visited_edges: list[set[str | int]]): +def traverse_path(graph: MultiDiGraph, node_a: str, node_b: str): path = nx.shortest_path(graph, node_a, node_b) path_edges = list(zip(path[:-1], path[1:])) edge_path= [(u, v, graph[u][v]) for u, v in path_edges] @@ -62,7 +57,7 @@ def apply_setting(graph: MultiDiGraph, setting_node: str, new_value: str): -def test_route(): +def test_route(settings: dict[Any, Any]): graph: MultiDiGraph = nx.nx_agraph.read_dot("bios-maps/asrock/b650e-riptide-wifi.gv") current_node = "Main" @@ -70,6 +65,6 @@ def test_route(): for category in settings: for setting_node in settings[category]: if graph.nodes[setting_node]["value"] != settings[category][setting_node]: - traverse_path(graph, current_node, setting_node, visited_edges) + traverse_path(graph, current_node, setting_node) current_node = setting_node apply_setting(graph, setting_node, settings[category][setting_node]) diff --git a/webui/ipkvm/util/hwinfo/__init__.py b/webui/ipkvm/util/hwinfo/__init__.py new file mode 100644 index 0000000..3cbdb70 --- /dev/null +++ b/webui/ipkvm/util/hwinfo/__init__.py @@ -0,0 +1,3 @@ +from .hwinfo import HWInfoMonitor + +hw_monitor = HWInfoMonitor() diff --git a/webui/ipkvm/util/hwinfo/hwinfo.py b/webui/ipkvm/util/hwinfo/hwinfo.py new file mode 100644 index 0000000..6eea2da --- /dev/null +++ b/webui/ipkvm/util/hwinfo/hwinfo.py @@ -0,0 +1,81 @@ +import threading +import re +import requests +from ipkvm.app import logger, ui +from ipkvm.util.profiles import profile_manager +import time +import pandas as pd + +class HWInfoMonitor(threading.Thread): + def __init__(self): + super().__init__() + self.request_url = f"http://{profile_manager.profile["client"]["hostname"]}:{profile_manager.profile["client"]["hwinfo_port"]}/json.json" + self.dataframe: pd.DataFrame + self.start() + + def run(self): + profile_manager.restart_hwinfo.wait() + while True: + profile_manager.restart_hwinfo.clear() + self.do_work() + + + def do_work(self): + self.create_dataframe() + time.sleep(0.25) + + def create_dataframe(self): + try: + request = requests.get(self.request_url, timeout=1) + + data = request.json() + + cpu_list: list[str] = [] + vid_list: list[str] = [] + mhz_list: list[str] = [] + ccd_list: list[str] = [] + temp_list: list[str] = [] + power_list: list[str] = [] + + for reading in data["hwinfo"]["readings"]: + label = reading["labelOriginal"] + + match = re.match(r"(?PCore[0-9]* \(CCD[0-9]\))|(?PCore [0-9]* VID)|(?PCore [0-9]* T0 Effective Clock)|(?PCore [0-9]* Power)", label) + + if match: + if match.group("core_ccd"): + core_ccd = match.group("core_ccd").split(' ') + core_ccd[0] = core_ccd[0][:4] + ' ' + core_ccd[0][4:] + cpu_list.append(core_ccd[0]) + ccd_list.append(core_ccd[1].strip('()')) + temp_list.append(round(reading["value"], 2)) + + elif match.group("core_vid"): + vid_list.append(reading["value"]) + + elif match.group("core_mhz"): + mhz_list.append(round(reading["value"], 2)) + + elif match.group("core_power"): + power_list.append(round(reading["value"], 2)) + + core_dataframe = pd.DataFrame({ + "CCD": ccd_list, + "Clk": mhz_list, + "VID": vid_list, + "Power": power_list, + "Temp": temp_list + }, index=cpu_list) + + ui.emit("update_core_info_table", core_dataframe.to_dict("index")) + + except Exception as e: + print(e) + + def is_hwinfo_alive(self): + request = requests.get(self.request_url, timeout=1) + + if request.status_code == 200 and len(request.json()) > 0: + return True + + return False diff --git a/webui/ipkvm/util/mkb/__init__.py b/webui/ipkvm/util/mkb/__init__.py new file mode 100644 index 0000000..8f071a7 --- /dev/null +++ b/webui/ipkvm/util/mkb/__init__.py @@ -0,0 +1,3 @@ +from .mkb import Esp32Serial + +esp32_serial = Esp32Serial() \ No newline at end of file diff --git a/webui/ipkvm/util/mkb/events.py b/webui/ipkvm/util/mkb/events.py new file mode 100644 index 0000000..87b334b --- /dev/null +++ b/webui/ipkvm/util/mkb/events.py @@ -0,0 +1,50 @@ +from ipkvm.app import ui +from . import esp32_serial +from .scancodes import HIDKeyCode, HIDMouseScanCodes + +@ui.on("get_serial_devices") +def handle_get_serial_devices(): + return esp32_serial.get_device_list() + +@ui.on('key_down') +def handle_keydown(data: str): + msg = { + "key_down": HIDKeyCode[data].value + } + + esp32_serial.mkb_queue.put(msg) + +@ui.on('key_up') +def handle_keyup(data: str): + msg = { + "key_up": HIDKeyCode[data].value + } + + esp32_serial.mkb_queue.put(msg) + +@ui.on("mouse_move") +def handle_mousemove(data: list[int]): + msg = { + "mouse_coord": { + "x": data[0], + "y": data[1] + } + } + + esp32_serial.mkb_queue.put(msg) + +@ui.on('mouse_down') +def handle_mousedown(data: int): + msg = { + "mouse_down": HIDMouseScanCodes[data] + } + + esp32_serial.mkb_queue.put(msg) + +@ui.on('mouse_up') +def handle_mouseup(data: int): + msg = { + "mouse_up": HIDMouseScanCodes[data] + } + + esp32_serial.mkb_queue.put(msg) \ No newline at end of file diff --git a/webui/ipkvm/util/mkb/mkb.py b/webui/ipkvm/util/mkb/mkb.py new file mode 100644 index 0000000..fae3890 --- /dev/null +++ b/webui/ipkvm/util/mkb/mkb.py @@ -0,0 +1,110 @@ +from enum import IntEnum +from os import name, listdir +import serial +from ipkvm.app import logger, ui +from ipkvm.util.profiles import profile_manager +import threading +from queue import Queue +import json +import time +from collections.abc import Mapping +from .post_codes import POSTTextDef, POSTHex7Segment +from .scancodes import HIDKeyCode + +class GPIO(IntEnum): + LOW = 0 + HIGH = 1 + +class Esp32Serial(threading.Thread): + def __init__(self): + super().__init__() + self.post_code_queue: Queue[str] = Queue() + self.mkb_queue: Queue[Mapping[str, int | str | Mapping[str, int]]] = Queue() + self._power_status = False + self._last_post_code = "00" + self.notify_code: str + self.active_notification_request = threading.Event() + self.post_code_notify = threading.Event() + + self.start() + + def run(self): + profile_manager.restart_serial.wait() + + while True: + profile_manager.restart_serial.clear() + self.do_work() + + + def do_work(self): + device = self.get_device() + with device as ser: + while not profile_manager.restart_serial.is_set(): + while not self.mkb_queue.empty(): + msg = self.mkb_queue.get() + ser.write(json.dumps(msg).encode()) + + while ser.in_waiting > 0: + try: + line = json.loads(ser.readline().decode().strip()) + + if "pwr" in line: + self._power_status = line["pwr"] + + elif "post_code" in line: + self._last_post_code = POSTHex7Segment[line["post_code"]] + + ui.emit("update_seven_segment", POSTHex7Segment[line["post_code"]]) + ui.emit("update_post_log", f"{POSTTextDef[line["post_code"]]}: {POSTHex7Segment[line["post_code"]]}") + + if self.active_notification_request.is_set(): + if self._last_post_code == self.notify_code: + self.post_code_notify.set() + self.active_notification_request.clear() + + except json.JSONDecodeError: + continue + + except UnicodeDecodeError: + continue + + time.sleep(0.01) + + def get_device(self): + if name == "posix": + assert isinstance(profile_manager.profile["server"], dict) + return serial.Serial(f"/dev/serial/by-id/{profile_manager.profile["server"]["esp32_serial"]}", 115200, + bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) + + else: + raise RuntimeError("Your OS is unsupported!") + + def ez_press_key(self, key: str): + msg = msg = { + "key_down": HIDKeyCode[key].value + } + + self.mkb_queue.put(msg) + + msg = msg = { + "key_up": HIDKeyCode[key].value + } + + self.mkb_queue.put(msg) + + def get_device_list(self): + if name == "posix": + serial_devices = listdir("/dev/serial/by-id/") + + else: + serial_devices = [] + + return serial_devices + + @property + def power_status(self): + return self._power_status + + @property + def last_post_code(self): + return self._last_post_code diff --git a/webui/ipkvm/util/mkb.py b/webui/ipkvm/util/mkb/post_codes.py similarity index 74% rename from webui/ipkvm/util/mkb.py rename to webui/ipkvm/util/mkb/post_codes.py index 36908a8..90e171c 100644 --- a/webui/ipkvm/util/mkb.py +++ b/webui/ipkvm/util/mkb/post_codes.py @@ -1,16 +1,3 @@ -from enum import IntEnum -from os import name -import serial -from ipkvm import profile -import threading -from queue import Queue -import json -import time -from ipkvm import ui -from collections.abc import Mapping - -# python can't use NUMBERS as enum keys?! - PoweredUpCodeDef = { 1: "System is entering S1 sleep state", 2: "System is entering S2 sleep state", @@ -543,245 +530,3 @@ POSTHex7Segment = { 254: "FE", 255: "FF" } - -HIDMouseScanCodes = { - 0: 1, - 2: 2, - 1: 4, - 3: 8, - 4: 16 -} - -ASCII2JS= { - "1": "Digit1", - "2": "Digit2", - "3": "Digit3", - "4": "Digit4", - "5": "Digit5", - "6": "Digit6", - "7": "Digit7", - "8": "Digit8", - "9": "Digit9", - "0": "Digit0", - - ".": "Period" -} - -class GPIO(IntEnum): - LOW = 0 - HIGH = 1 - -# God Bless CHADGPT -class HIDKeyCode(IntEnum): - """ - Enum that translates modern JS key.code andvalues to HID scancodes. - """ - # Letter keys (A-Z) - KeyA = 4 - KeyB = 5 - KeyC = 6 - KeyD = 7 - KeyE = 8 - KeyF = 9 - KeyG = 10 - KeyH = 11 - KeyI = 12 - KeyJ = 13 - KeyK = 14 - KeyL = 15 - KeyM = 16 - KeyN = 17 - KeyO = 18 - KeyP = 19 - KeyQ = 20 - KeyR = 21 - KeyS = 22 - KeyT = 23 - KeyU = 24 - KeyV = 25 - KeyW = 26 - KeyX = 27 - KeyY = 28 - KeyZ = 29 - - # Number keys (top row) - Digit1 = 30 - Digit2 = 31 - Digit3 = 32 - Digit4 = 33 - Digit5 = 34 - Digit6 = 35 - Digit7 = 36 - Digit8 = 37 - Digit9 = 38 - Digit0 = 39 - - # Control keys - Enter = 40 - Escape = 41 - Backspace = 42 - Tab = 43 - Space = 44 - - Minus = 45 - Equal = 46 - BracketLeft = 47 - BracketRight = 48 - Backslash = 49 - - # Punctuation keys - Semicolon = 51 - Quote = 52 - Backquote = 53 - Comma = 54 - Period = 55 - Slash = 56 - - CapsLock = 57 - - # Function keys (F1-F12) - F1 = 58 - F2 = 59 - F3 = 60 - F4 = 61 - F5 = 62 - F6 = 63 - F7 = 64 - F8 = 65 - F9 = 66 - F10 = 67 - F11 = 68 - F12 = 69 - - PrintScreen = 70 - ScrollLock = 71 - Pause = 72 - - Insert = 73 - Home = 74 - PageUp = 75 - - Delete = 76 - End = 77 - PageDown = 78 - - ArrowRight = 79 - ArrowLeft = 80 - ArrowDown = 81 - ArrowUp = 82 - - # Numpad keys - NumLock = 83 - NumpadDivide = 84 - NumpadMultiply = 85 - NumpadSubtract = 86 - NumpadAdd = 87 - NumpadEnter = 88 - Numpad1 = 89 - Numpad2 = 90 - Numpad3 = 91 - Numpad4 = 92 - Numpad5 = 93 - Numpad6 = 94 - Numpad7 = 95 - Numpad8 = 96 - Numpad9 = 97 - Numpad0 = 98 - NumpadDecimal = 99 - - # Additional keys - IntlBackslash = 100 - ContextMenu = 101 - Power = 102 - - # Modifier keys - ControlLeft = 224 - ShiftLeft = 225 - AltLeft = 226 - MetaLeft = 227 # Windows / Command key (left) - ControlRight = 228 - ShiftRight = 229 - AltRight = 230 - MetaRight = 231 # Windows / Command key (right) - -class Esp32Serial(threading.Thread): - def __init__(self): - super().__init__() - self.post_code_queue: Queue[str] = Queue() - self.mkb_queue: Queue[Mapping[str, int | str | Mapping[str, int]]] = Queue() - self.change_serial_device = threading.Event() - self.device = self.get_device() - self._power_status = False - self._last_post_code = "00" - self.notify_code: str - self.active_notification_request = threading.Event() - self.post_code_notify = threading.Event() - - self.start() - - def run(self): - with self.device as ser: - while True: - # if self.change_serial_device.is_set(): - # self.change_serial_device.clear() - # self.device = self.get_device() - - while not self.mkb_queue.empty(): - msg = self.mkb_queue.get() - ser.write(json.dumps(msg).encode()) - - while ser.in_waiting > 0: - try: - line = json.loads(ser.readline().decode().strip()) - - if "pwr" in line: - self._power_status = line["pwr"] - - elif "post_code" in line: - self._last_post_code = POSTHex7Segment[line["post_code"]] - - ui.emit("update_seven_segment", POSTHex7Segment[line["post_code"]]) - ui.emit("update_post_log", f"{POSTTextDef[line["post_code"]]}: {POSTHex7Segment[line["post_code"]]}") - - if self.active_notification_request.is_set(): - if self._last_post_code == self.notify_code: - self.post_code_notify.set() - self.active_notification_request.clear() - - except json.JSONDecodeError: - continue - - except UnicodeDecodeError: - continue - # self.post_code_queue.put(ser.read().hex()) - - time.sleep(0.01) - - def get_device(self): - if name == "posix": - return serial.Serial(f"/dev/serial/by-id/{profile["server"]["esp32_serial"]}", 115200, bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) - - else: - raise RuntimeError("Your OS is unsupported!") - - def ez_press_key(self, key: str): - msg = msg = { - "key_down": HIDKeyCode[key].value - } - - self.mkb_queue.put(msg) - - msg = msg = { - "key_up": HIDKeyCode[key].value - } - - self.mkb_queue.put(msg) - - @property - def power_status(self): - return self._power_status - - @property - def last_post_code(self): - return self._last_post_code diff --git a/webui/ipkvm/util/mkb/scancodes.py b/webui/ipkvm/util/mkb/scancodes.py new file mode 100644 index 0000000..5aa6045 --- /dev/null +++ b/webui/ipkvm/util/mkb/scancodes.py @@ -0,0 +1,157 @@ +from enum import IntEnum + +HIDMouseScanCodes = { + 0: 1, + 2: 2, + 1: 4, + 3: 8, + 4: 16 +} + +ASCII2JS= { + "1": "Digit1", + "2": "Digit2", + "3": "Digit3", + "4": "Digit4", + "5": "Digit5", + "6": "Digit6", + "7": "Digit7", + "8": "Digit8", + "9": "Digit9", + "0": "Digit0", + + ".": "Period" +} + +# God Bless CHADGPT +class HIDKeyCode(IntEnum): + """ + Enum that translates modern JS key.code andvalues to HID scancodes. + """ + # Letter keys (A-Z) + KeyA = 4 + KeyB = 5 + KeyC = 6 + KeyD = 7 + KeyE = 8 + KeyF = 9 + KeyG = 10 + KeyH = 11 + KeyI = 12 + KeyJ = 13 + KeyK = 14 + KeyL = 15 + KeyM = 16 + KeyN = 17 + KeyO = 18 + KeyP = 19 + KeyQ = 20 + KeyR = 21 + KeyS = 22 + KeyT = 23 + KeyU = 24 + KeyV = 25 + KeyW = 26 + KeyX = 27 + KeyY = 28 + KeyZ = 29 + + # Number keys (top row) + Digit1 = 30 + Digit2 = 31 + Digit3 = 32 + Digit4 = 33 + Digit5 = 34 + Digit6 = 35 + Digit7 = 36 + Digit8 = 37 + Digit9 = 38 + Digit0 = 39 + + # Control keys + Enter = 40 + Escape = 41 + Backspace = 42 + Tab = 43 + Space = 44 + + Minus = 45 + Equal = 46 + BracketLeft = 47 + BracketRight = 48 + Backslash = 49 + + # Punctuation keys + Semicolon = 51 + Quote = 52 + Backquote = 53 + Comma = 54 + Period = 55 + Slash = 56 + + CapsLock = 57 + + # Function keys (F1-F12) + F1 = 58 + F2 = 59 + F3 = 60 + F4 = 61 + F5 = 62 + F6 = 63 + F7 = 64 + F8 = 65 + F9 = 66 + F10 = 67 + F11 = 68 + F12 = 69 + + PrintScreen = 70 + ScrollLock = 71 + Pause = 72 + + Insert = 73 + Home = 74 + PageUp = 75 + + Delete = 76 + End = 77 + PageDown = 78 + + ArrowRight = 79 + ArrowLeft = 80 + ArrowDown = 81 + ArrowUp = 82 + + # Numpad keys + NumLock = 83 + NumpadDivide = 84 + NumpadMultiply = 85 + NumpadSubtract = 86 + NumpadAdd = 87 + NumpadEnter = 88 + Numpad1 = 89 + Numpad2 = 90 + Numpad3 = 91 + Numpad4 = 92 + Numpad5 = 93 + Numpad6 = 94 + Numpad7 = 95 + Numpad8 = 96 + Numpad9 = 97 + Numpad0 = 98 + NumpadDecimal = 99 + + # Additional keys + IntlBackslash = 100 + ContextMenu = 101 + Power = 102 + + # Modifier keys + ControlLeft = 224 + ShiftLeft = 225 + AltLeft = 226 + MetaLeft = 227 # Windows / Command key (left) + ControlRight = 228 + ShiftRight = 229 + AltRight = 230 + MetaRight = 231 # Windows / Command key (right) diff --git a/webui/ipkvm/util/profiles/__init__.py b/webui/ipkvm/util/profiles/__init__.py new file mode 100644 index 0000000..46a353c --- /dev/null +++ b/webui/ipkvm/util/profiles/__init__.py @@ -0,0 +1,3 @@ +from .profiles import ProfileManager + +profile_manager = ProfileManager() \ No newline at end of file diff --git a/webui/ipkvm/util/profiles/events.py b/webui/ipkvm/util/profiles/events.py new file mode 100644 index 0000000..3bf4dd2 --- /dev/null +++ b/webui/ipkvm/util/profiles/events.py @@ -0,0 +1,15 @@ +import tomlkit +from ipkvm.app import ui +from . import profile_manager + +@ui.on("get_current_profile") +def handle_current_profile(): + return tomlkit.dumps(profile_manager.profile) + +@ui.on("save_profile") +def handle_save_profile(data: str): + profile_manager.save_profile(tomlkit.parse(data)) # type: ignore + +@ui.on("save_profile_as") +def handle_save_profile_as(data: str, name: str): + profile_manager.save_profile(tomlkit.parse(data), name) # type: ignore \ No newline at end of file diff --git a/webui/ipkvm/util/profiles/profiles.py b/webui/ipkvm/util/profiles/profiles.py new file mode 100644 index 0000000..c13d4ad --- /dev/null +++ b/webui/ipkvm/util/profiles/profiles.py @@ -0,0 +1,96 @@ +from os import listdir +import threading +import tomlkit +from ipkvm.app import logger, ui +from typing import TypedDict + +class VideoDict(TypedDict): + friendly_name: str + resolution: str + fps: str + +class ServerDict(TypedDict): + esp32_serial: str + video_device: VideoDict + +class ClientDict(TypedDict): + hostname: str + hwinfo_port: str + overclocking: dict[str, dict[str, str]] + +class ProfileDict(TypedDict): + server: ServerDict + client: ClientDict + +class ProfileManager(): + def __init__(self): + self.restart_serial = threading.Event() + self.restart_video = threading.Event() + self.restart_hwinfo = threading.Event() + self._cur_profile_name: str = "" + self._profiles = listdir("profiles") + + if len(self._profiles) == 0: + logger.info("No profiles found, loading default profile.") + # For all intents and purposes, in this code, the profiles are treated like dictionaries... + # But idk how to make pylance happy in all cases here. + self._profile: ProfileDict = self.load_default_profile() # type: ignore + + elif len(self._profiles) >= 1: + logger.info(f"Autoloading a profile: {self._profiles[0]}...") + + self.load_profile(self._profiles[0]) # type: ignore + + def load_default_profile(self): + with open("webui/ipkvm/templates/default.toml", 'r') as file: + self._cur_profile_name = "default.toml" + return tomlkit.parse(file.read()) + + def load_profile(self, name: str): + with open(f"profiles/{name}", 'r') as file: + self._cur_profile_name = name + self._profile = tomlkit.parse(file.read()) # type: ignore + self.notify_all() + + def save_profile(self, new_profile: ProfileDict, name: str = ""): + if name == "": + name = self._cur_profile_name + + else: + if not name.endswith(".toml"): + name += ".toml" + + with open(f"profiles/{name}", 'w') as file: + # In case you do a save-as and change the name! + self._cur_profile_name = name + tomlkit.dump(new_profile, file) + self._profiles = listdir("profiles") + + if new_profile["server"]["esp32_serial"] != self._profile["server"]["esp32_serial"]: + self.restart_serial.set() + + if new_profile["server"]["video_device"] != self._profile["server"]["video_device"]: + self.restart_video.set() + + if (new_profile["client"]["hostname"] != self._profile["client"]["hostname"] or + new_profile["client"]["hwinfo_port"] != self._profile["client"]["hwinfo_port"]): + self.restart_video.set() + + self._profile = new_profile + + def notify_all(self): + self.restart_serial.set() + self.restart_video.set() + self.restart_hwinfo.set() + + @property + def cur_profile_name(self): + return self._cur_profile_name + + @property + def profile_list(self): + return self._profiles + + @property + def profile(self): + return self._profile diff --git a/webui/ipkvm/util/video.py b/webui/ipkvm/util/video.py deleted file mode 100644 index 6559e8c..0000000 --- a/webui/ipkvm/util/video.py +++ /dev/null @@ -1,139 +0,0 @@ -"""os.name provides the type of the operating system!""" -from os import name, listdir -import dataclasses -import re -import subprocess -import threading -import av -import av.container -import cv2 -from ipkvm import logger - -@dataclasses.dataclass -class VideoDevice: - """ - Container for video input device data. - """ - friendly_name: str - path: str - video_formats: dict[str, dict[str, list[float]]] - -def check_valid_device_linux(devices: list[str], valid_cameras: dict[str, str]): - """ - Uses v4l2-ctl to determine whether a video device actually provides video. - Takes list of /dev/videoX strings. - Returns a dictionary of /dev/videoX strings as keys and friendly device names as values. - """ - for device in devices: - cmd = ["v4l2-ctl", "-d", device, "--all"] - lines = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - check=True).stdout.decode().strip().splitlines() - - if v4l2_check_capability(lines): - for line in lines: - if "Model" in line: - model_name = line.split(':')[1].strip() - valid_cameras.update({model_name: device}) - - return valid_cameras - -def v4l2_check_capability(lines: list[str]): - """ - Checks if the provided stdout from v4l2-ctl identifies a device as a video capture device. - """ - for i, line in enumerate(lines): - if "Device Caps" in line: - x = i - while "Media Driver Info" not in lines[x]: - x += 1 - if "Video Capture" in lines[x]: - return True - - return False - -def scan_devices(): - """ - Creates a list of valid video devices and returns a dictionary of friendly names and device paths. - """ - valid_devices: dict[str, str] = {} - - if name == "posix": - devices: list[str] = [f"/dev/{x}" for x in listdir("/dev/") if "video" in x] - valid_devices = check_valid_device_linux(devices, valid_devices) - - elif name == "nt": - # implement camera acqisition for windows - pass - - return valid_devices - -def get_video_formats(device: str): - """ - Use v4l2-ctl (Linux) or FFplay (Windows) to get camera operating modes and sort by quality. - """ - video_formats: dict[str, dict[str, list[str]]] = {} - # Translates fourcc codec labels to the type that FFmpeg uses, and blacklists bad codecs. - fourcc_format_translation: dict[str, str | None] = { - "YUYV": None, - "MJPG": "mjpeg" - } - - if name == "posix": - cmd = ["v4l2-ctl", "-d", device, "--list-formats-ext"] - output = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True).stdout.decode().strip() - matches = re.finditer(r"(?P'\S*')|(?P\d*x\d*)|(?P\d*.\d* fps)", output) - - if matches: - video_format = None - resolution = None - fps = None - - for match in matches: - if re.match(r"'\S*'", match[0]): - video_format = fourcc_format_translation[match[0].strip('\'')] - resolution = None - fps = None - elif re.match(r"\d*x\d*", match[0]): - resolution = match[0] - fps = None - elif re.match(r"\d*.\d* fps", match[0]): - fps = match[0].rstrip(" fps") - - if video_format and resolution and fps: - if video_format not in video_formats: - video_formats.update({ - video_format: { - resolution: [fps] - } - }) - elif resolution not in video_formats[video_format]: - video_formats[video_format].update({ - resolution: [fps] - }) - else: - video_formats[video_format][resolution].append(fps) - - return video_formats - -def create_device_list(): - """ - Create a complete device list including name, device ID, and available video formats. - """ - device_names = scan_devices() - device_list: list[VideoDevice] = [] - - for device_name in device_names: - device_list.append(VideoDevice(device_name, device_names[device_name], get_video_formats(device_names[device_name]))) - if len(device_list[-1].video_formats) == 0: - device_list.pop() - - if len(device_list) > 0: - logger.info(f"Found {len(device_list)} video devices.") - return device_list - - else: - raise RuntimeError("No video devices found on this system!") - -# EZ DEBUGGING -if __name__ == '__main__': - print(create_device_list()) diff --git a/webui/ipkvm/util/video/__init__.py b/webui/ipkvm/util/video/__init__.py new file mode 100644 index 0000000..58378df --- /dev/null +++ b/webui/ipkvm/util/video/__init__.py @@ -0,0 +1,3 @@ +from .video import FrameBuffer + +frame_buffer = FrameBuffer() \ No newline at end of file diff --git a/webui/ipkvm/util/video/video.py b/webui/ipkvm/util/video/video.py new file mode 100644 index 0000000..f5ab90c --- /dev/null +++ b/webui/ipkvm/util/video/video.py @@ -0,0 +1,263 @@ +from os import listdir, name as os_name +import dataclasses +import re +import subprocess +import threading +import av +import av.container +import cv2 +from ipkvm.app import logger, ui +from ipkvm.util.profiles import profile_manager +import time +from PIL import Image +import io + +FourCCtoFFMPEG = { + "yuyv422": "YUYV", + "mjpeg": "MJPG" +} + +class FrameBuffer(threading.Thread): + def __init__(self): + super().__init__() + self.buffer_lock = threading.Lock() + img = Image.open("webui/ipkvm/static/Bsodwindows10.png") + buffer = io.BytesIO() + img.save(buffer, format="JPEG") + jpeg_bytes = buffer.getvalue() # This contains the JPEG image as bytes + self.cur_frame = jpeg_bytes + self.new_frame = threading.Event() + self.new_frame.set() + self.start() + + def run(self): + while not profile_manager.restart_video.is_set(): + img = Image.open("webui/ipkvm/static/Bsodwindows10.png") + buffer = io.BytesIO() + img.save(buffer, format="JPEG") + jpeg_bytes = buffer.getvalue() # This contains the JPEG image as bytes + self.cur_frame = jpeg_bytes + self.new_frame.set() + time.sleep(1) + + while True: + profile_manager.restart_video.clear() + self.capture_feed() + + + def capture_feed(self): + device = self.acquire_device() + while not profile_manager.restart_video.is_set(): + # try: + # for frame in device.decode(video=0): + # frame = frame.to_ndarray(format='rgb24') + # ret, self.cur_frame = cv2.imencode('.jpg', frame) + # print(ret) + # cv2.imwrite("test.jpg", frame) + # except av.BlockingIOError: + # pass + + success, frame = device.read() + if not success: + break + else: + # ret, buffer = cv2.imencode('.jpg', frame) + # self.cur_frame = buffer.tobytes() + # Convert BGR (OpenCV) to RGB (PIL) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Convert to PIL Image + img = Image.fromarray(frame_rgb) + + # Save to a bytes buffer (for in-memory use) + buffer = io.BytesIO() + img.save(buffer, format="JPEG") + jpeg_bytes = buffer.getvalue() # This contains the JPEG image as bytes + self.cur_frame = jpeg_bytes + + self.new_frame.set() + + device.release() + + + # def acquire_device(self): + # device_list = video.create_device_list() + # device_path = "" + # for device in device_list: + # if device.friendly_name == profile["video_device"]["friendly_name"]: + # device_path = device.path +# + # if name == "posix": + # return av.open(device_path, format="video4linux2", container_options={ + # "framerate": profile["video_device"]["fps"], + # "video_size": profile["video_device"]["resolution"], + # "input_format": profile["video_device"]["format"] + # }) +# + # else: + # raise RuntimeError("We're on something other than Linux, and that's not yet supported!") + + def acquire_device(self): + device_list = create_device_list() + device_path = "" + for device_name in device_list: + if device_name == profile_manager.profile["server"]["video_device"]["friendly_name"]: + device_path = device_list[device_name]["path"] + break + + video_device = cv2.VideoCapture(device_path) # Use default webcam (index 0) + + video_device.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*FourCCtoFFMPEG[device_list[device_name]["codec"]])) + video_device.set(cv2.CAP_PROP_FRAME_WIDTH, int(profile_manager.profile["server"]["video_device"]["resolution"].split('x')[0])) + video_device.set(cv2.CAP_PROP_FRAME_HEIGHT, int(profile_manager.profile["server"]["video_device"]["resolution"].split('x')[1])) + video_device.set(cv2.CAP_PROP_FPS, float(profile_manager.profile["server"]["video_device"]["fps"])) + + return video_device + +@dataclasses.dataclass +class VideoDevice: + """ + Container for video input device data. + """ + friendly_name: str + path: str + video_formats: dict[str, dict[str, list[float]]] + +def check_valid_device_linux(devices: list[str], valid_cameras: dict[str, str]): + """ + Uses v4l2-ctl to determine whether a video device actually provides video. + Takes list of /dev/videoX strings. + Returns a dictionary of /dev/videoX strings as keys and friendly device names as values. + """ + for device in devices: + cmd = ["v4l2-ctl", "-d", device, "--all"] + lines = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True).stdout.decode().strip().splitlines() + + if v4l2_check_capability(lines): + for line in lines: + if "Model" in line: + model_name = line.split(':')[1].strip() + valid_cameras.update({model_name: device}) + + return valid_cameras + +def v4l2_check_capability(lines: list[str]): + """ + Checks if the provided stdout from v4l2-ctl identifies a device as a video capture device. + """ + for i, line in enumerate(lines): + if "Device Caps" in line: + x = i + while "Media Driver Info" not in lines[x]: + x += 1 + if "Video Capture" in lines[x]: + return True + + return False + +def scan_devices(): + """ + Creates a list of valid video devices and returns a dictionary of friendly names and device paths. + """ + valid_devices: dict[str, str] = {} + + if os_name == "posix": + devices: list[str] = [f"/dev/{x}" for x in listdir("/dev/") if "video" in x] + valid_devices = check_valid_device_linux(devices, valid_devices) + + elif os_name == "nt": + # implement camera acqisition for windows + pass + + return valid_devices + +def get_video_formats(device: str): + """ + Use v4l2-ctl (Linux) or FFplay (Windows) to get camera operating modes and sort by quality. + """ + video_formats: dict[str, dict[str, list[str]]] = {} + + # Translates fourcc codec labels to the type that FFmpeg uses + fourcc_format_translation: dict[str, str | None] = { + "YUYV": "yuyv422", + "MJPG": "mjpeg" + } + + ranked_formats = { + "yuyv422": 1, + "mjpeg": 2 + } + + if os_name == "posix": + cmd = ["v4l2-ctl", "-d", device, "--list-formats-ext"] + output = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True).stdout.decode().strip() + matches = re.finditer(r"(?P'\S*')|(?P\d*x\d*)|(?P\d*.\d* fps)", output) + + if matches: + video_format = None + resolution = None + fps = None + + for match in matches: + if re.match(r"'\S*'", match[0]): + video_format = fourcc_format_translation[match[0].strip('\'')] + resolution = None + fps = None + elif re.match(r"\d*x\d*", match[0]): + resolution = match[0] + fps = None + elif re.match(r"\d*.\d* fps", match[0]): + fps = match[0].rstrip(" fps") + + if video_format and resolution and fps: + if video_format not in video_formats: + video_formats.update({ + video_format: { + resolution: [fps] + } + }) + elif resolution not in video_formats[video_format]: + video_formats[video_format].update({ + resolution: [fps] + }) + else: + video_formats[video_format][resolution].append(fps) + + for ranked_format in ranked_formats: + if ranked_format in video_formats: + #video_formats = {key: video_formats[key] for key in video_formats if key == ranked_format} + video_formats = { + "codec": ranked_format, + "formats": video_formats[ranked_format] + } + + return video_formats + +def create_device_list(): + """ + Create a complete device list including name, device ID, and available video formats. + """ + device_names = scan_devices() + device_list: list[VideoDevice] = [] + devices = {} + + for device_name in device_names: + # device_list.append(VideoDevice(device_name, device_names[device_name], get_video_formats(device_names[device_name]))) + devices[device_name] = { + "path": device_names[device_name] + } + devices[device_name].update(get_video_formats(device_names[device_name])) + #if len(device_list[-1].video_formats) == 0: + # device_list.pop() + + if len(devices) > 0: + # logger.info(f"Found {len(device_list)} video devices.") + return devices + + else: + raise RuntimeError("No video devices found on this system!") + +@ui.on("get_video_devices") +def handle_get_video_devices(): + return create_device_list() diff --git a/webui/launch.py b/webui/launch.py index 1ded817..6d65c99 100644 --- a/webui/launch.py +++ b/webui/launch.py @@ -1,5 +1,4 @@ -from ipkvm import app, ui, states +from ipkvm.app import app, ui if __name__ == '__main__': - ui.run(app, host='0.0.0.0', port=5000) - #pass \ No newline at end of file + ui.run(app, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/webui/serial_test.py b/webui/serial_test.py deleted file mode 100644 index bf20a75..0000000 --- a/webui/serial_test.py +++ /dev/null @@ -1,70 +0,0 @@ -import serial -import sys -import datetime -import json -import time - -test_json_a = { - "mouseX": 99999, - "mouseY": 99999, - "mouse_down": ["rbutton", "lbutton"], - "mouse_up": ["otherbutton"], - "key_up": [], - "key_down": [11, 12] -} - -test_json_b = { - "mouseX": 99999, - "mouseY": 99999, - "mouse_down": ["rbutton", "lbutton"], - "mouse_up": ["otherbutton"], - "key_up": [11, 12], - "key_down": [] -} - -test_json_c = { - "mouse_coord": { - "x": 100, - "y": 100, - } -} - -test_json_d = { - "mouse_coord": { - "x": 32000, - "y": 32000, - } -} - -def read_serial(port): - try: - # Open the serial port - with serial.Serial(port, 115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE) as ser: - print(f"Listening on {port} at 115200 baud...") - line = ser.readline().decode().strip() - while True: - # Read a line from the serial port - while ser.in_waiting > 0: - line = ser.readline().decode().strip() - line = json.loads(line) - print(line) - # print(json.loads(ser.read_all())) - # Print the raw data - #ser.write(json.dumps(test_json_c).encode()) - #time.sleep(1) - #ser.write(json.dumps(test_json_d).encode()) - #time.sleep(1) - except serial.SerialException as e: - print(f"Error: {e}") - except KeyboardInterrupt: - print("Exiting...") - sys.exit() - -if __name__ == "__main__": - #if len(sys.argv) != 2: - # print("Usage: python read_serial.py ") - # print("Example: python read_serial.py COM3 (Windows) or /dev/ttyUSB0 (Linux)") - # sys.exit(1) - - #port_name = sys.argv[1] - read_serial('/dev/serial/by-id/usb-1a86_USB_Single_Serial_585D015807-if00') diff --git a/webui/state_test.py b/webui/state_test.py deleted file mode 100644 index 6e34d50..0000000 --- a/webui/state_test.py +++ /dev/null @@ -1,47 +0,0 @@ -from enum import Enum - -from transitions.experimental.utils import with_model_definitions, event, add_transitions, transition -from transitions import Machine - - -class State(Enum): - A = "A" - B = "B" - C = "C" - - -class Model: - - state: State = State.A - - @add_transitions(transition(source=State.B, dest=State.A)) - def foo(self): ... - - @add_transitions(transition(source=State.C, dest=State.A)) - def fod(self): ... - - @add_transitions(transition(source=State.A, dest=State.B)) - def fud(self): ... - - bar = event( - {"source": State.B, "dest": State.A, "conditions": lambda: False}, - transition(source=State.B, dest=State.C) - ) - - -@with_model_definitions # don't forget to define your model with this decorator! -class MyMachine(Machine): - pass - - -model = Model() -machine = MyMachine(model, states=State, initial=model.state) -print(model.state) -model.fud() -print(model.state) -model.bar() -print(model.state) -assert model.state == State.C -model.fod() -print(model.state) -assert model.state == State.A \ No newline at end of file