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 0000000..8845ddf Binary files /dev/null and b/webui/ipkvm/static/Bsodwindows10.png differ 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