huuuuge organizational changes, added profile editing via webui, added hwinfo monitoring in webui, config changes work live
This commit is contained in:
parent
7f5812e1b7
commit
0d35fb76e0
28
profiles/default.toml
Normal file
28
profiles/default.toml
Normal 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"
|
@ -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")
|
62
webui/app.py
62
webui/app.py
@ -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)
|
||||
|
@ -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)
|
@ -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
8
webui/ipkvm/app.py
Normal 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)
|
@ -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)
|
@ -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
|
@ -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
|
@ -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
|
||||
|
||||
|
||||
|
0
webui/ipkvm/states/__init__.py
Normal file
0
webui/ipkvm/states/__init__.py
Normal file
38
webui/ipkvm/states/events.py
Normal file
38
webui/ipkvm/states/events.py
Normal 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()
|
@ -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)
|
BIN
webui/ipkvm/static/Bsodwindows10.png
Executable file
BIN
webui/ipkvm/static/Bsodwindows10.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
@ -68,4 +68,8 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.first-option {
|
||||
display:none;
|
||||
}
|
@ -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
|
||||
})
|
||||
|
||||
|
97
webui/ipkvm/static/js/profiles.js
Normal file
97
webui/ipkvm/static/js/profiles.js
Normal 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);
|
@ -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) => {
|
||||
|
28
webui/ipkvm/templates/default.toml
Normal file
28
webui/ipkvm/templates/default.toml
Normal 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"
|
@ -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">
|
||||
|
@ -1 +1,3 @@
|
||||
|
||||
from . import hwinfo
|
||||
from .mkb import events
|
||||
from .profiles import events
|
||||
|
0
webui/ipkvm/util/graphs/__init__.py
Normal file
0
webui/ipkvm/util/graphs/__init__.py
Normal 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])
|
3
webui/ipkvm/util/hwinfo/__init__.py
Normal file
3
webui/ipkvm/util/hwinfo/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .hwinfo import HWInfoMonitor
|
||||
|
||||
hw_monitor = HWInfoMonitor()
|
81
webui/ipkvm/util/hwinfo/hwinfo.py
Normal file
81
webui/ipkvm/util/hwinfo/hwinfo.py
Normal 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
|
3
webui/ipkvm/util/mkb/__init__.py
Normal file
3
webui/ipkvm/util/mkb/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .mkb import Esp32Serial
|
||||
|
||||
esp32_serial = Esp32Serial()
|
50
webui/ipkvm/util/mkb/events.py
Normal file
50
webui/ipkvm/util/mkb/events.py
Normal 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
110
webui/ipkvm/util/mkb/mkb.py
Normal 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
|
@ -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
|
157
webui/ipkvm/util/mkb/scancodes.py
Normal file
157
webui/ipkvm/util/mkb/scancodes.py
Normal 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)
|
3
webui/ipkvm/util/profiles/__init__.py
Normal file
3
webui/ipkvm/util/profiles/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .profiles import ProfileManager
|
||||
|
||||
profile_manager = ProfileManager()
|
15
webui/ipkvm/util/profiles/events.py
Normal file
15
webui/ipkvm/util/profiles/events.py
Normal 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
|
96
webui/ipkvm/util/profiles/profiles.py
Normal file
96
webui/ipkvm/util/profiles/profiles.py
Normal 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
|
@ -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())
|
3
webui/ipkvm/util/video/__init__.py
Normal file
3
webui/ipkvm/util/video/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .video import FrameBuffer
|
||||
|
||||
frame_buffer = FrameBuffer()
|
263
webui/ipkvm/util/video/video.py
Normal file
263
webui/ipkvm/util/video/video.py
Normal 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()
|
@ -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)
|
@ -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')
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user