huuuuge organizational changes, added profile editing via webui, added hwinfo monitoring in webui, config changes work live

This commit is contained in:
rawhide kobayashi 2025-03-12 00:18:27 -05:00
parent 7f5812e1b7
commit 0d35fb76e0
Signed by: rawhide_k
GPG Key ID: E71F77DDBC513FD7
40 changed files with 1025 additions and 1131 deletions

28
profiles/default.toml Normal file
View File

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

View File

@ -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 <port>")
# 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")

View File

@ -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 """
<html>
<head>
<title>Webcam Stream</title>
</head>
<body>
<h1>Webcam Stream</h1>
<img src="/video_feed">
</body>
</html>
"""
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

View File

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

View File

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

8
webui/ipkvm/app.py Normal file
View File

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

View File

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

View File

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

View File

@ -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"(?P<core_ccd>Core[0-9]* \(CCD[0-9]\))|(?P<core_vid>Core [0-9]* VID)|(?P<core_mhz>Core [0-9]* T0 Effective Clock)|(?P<core_power>Core [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

View File

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

View File

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -68,4 +68,8 @@
bottom: 0;
left: 0;
overflow: auto;
}
.first-option {
display:none;
}

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

@ -13,6 +13,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/vendor/handsontable/ht-theme-main.min.css') }}">
<script src="{{ url_for('static', filename='js/vendor/handsontable/handsontable.full.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
<script src="{{ url_for('static', filename='js/profiles.js') }}"></script>
<html>
@ -24,26 +25,28 @@
<div class="row-flex-container full-screen">
<div class="column-flex-container left-third">
<div class="menubar">
<!-- <div class="dropdown">
<button type="button">Load profile...</button>
<div class="dropdown-options">
<p>Test1</p>
<p>Test2</p>
</div>
</div> -->
<select id="countrySelect" name="countrySelect">
<option disabled selected>Load profile...</option>
<option>USA</option>
<option>Germany</option>
<option>France</option>
<select id="profile-select">
<option disabled selected class="first-option">Load profile...</option>
</select>
<button type="button" onclick="socket.emit(`save_profile`);">Save</button>
<button type="button" onclick="socket.emit(`save_new_profile`);">Save as</button>
<select id="serial-select" onchange="code_mirror.setValue(update_toml('esp32_serial', this.value));"">
<option disabled selected class="first-option">Serial device...</option>
</select>
<select id="video-select" onchange="code_mirror.setValue(update_toml('friendly_name', this.value)); populate_resolution(this.value);">
<option disabled selected class="first-option">Video device...</option>
</select>
<select id="resolution-select" onchange="code_mirror.setValue(update_toml('resolution', this.value)); populate_fps(this.value, videoDropdown.value);">
<option disabled selected class="first-option">Resolution...</option>
</select>
<select id="fps-select" onchange="code_mirror.setValue(update_toml('fps', this.value));">
<option disabled selected class="first-option">FPS...</option>
</select>
<button type="button" onclick="socket.emit('save_profile', code_mirror.getValue());">Save</button>
<button type="button" onclick="save_new_profile();">Save as</button>
</div>
<div class="cm-editor">
<div id="codemirror" class="cm-scroller"></div>
</div>
<div id="stats-table" class="ht-theme-main"></div>
<div id="stats-table" class="ht-theme-main">Waiting for data...</div>
</div>
<div class="right-third">
<div class="row-flex-container">

View File

@ -1 +1,3 @@
from . import hwinfo
from .mkb import events
from .profiles import events

View File

View File

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

View File

@ -0,0 +1,3 @@
from .hwinfo import HWInfoMonitor
hw_monitor = HWInfoMonitor()

View File

@ -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"(?P<core_ccd>Core[0-9]* \(CCD[0-9]\))|(?P<core_vid>Core [0-9]* VID)|(?P<core_mhz>Core [0-9]* T0 Effective Clock)|(?P<core_power>Core [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

View File

@ -0,0 +1,3 @@
from .mkb import Esp32Serial
esp32_serial = Esp32Serial()

View File

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

110
webui/ipkvm/util/mkb/mkb.py Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
from .profiles import ProfileManager
profile_manager = ProfileManager()

View File

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

View File

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

View File

@ -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<format>'\S*')|(?P<resolution>\d*x\d*)|(?P<fps>\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())

View File

@ -0,0 +1,3 @@
from .video import FrameBuffer
frame_buffer = FrameBuffer()

View File

@ -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<format>'\S*')|(?P<resolution>\d*x\d*)|(?P<fps>\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()

View File

@ -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
ui.run(app, host='0.0.0.0', port=5000)

View File

@ -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 <port>")
# 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')

View File

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