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 ipkvm import routes
|
||||||
from flask import Flask
|
|
||||||
from flask_socketio import SocketIO
|
|
||||||
import tomlkit
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
__all__ = ["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
|
|
||||||
|
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.app import app
|
||||||
from ipkvm import frame_buffer
|
from ipkvm.util.video import frame_buffer
|
||||||
from flask import Response, render_template
|
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
|
import time
|
||||||
from transitions.experimental.utils import with_model_definitions, add_transitions, transition
|
from transitions.experimental.utils import with_model_definitions, add_transitions, transition
|
||||||
from transitions.extensions import GraphMachine
|
from transitions.extensions import GraphMachine
|
||||||
from ipkvm import esp32_serial
|
from ipkvm.util import esp32_serial
|
||||||
from ipkvm.util.mkb import GPIO, HIDKeyCode
|
from ipkvm.util.mkb import GPIO, HIDKeyCode
|
||||||
import logging
|
from ipkvm.app import logging, ui
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
# Set transitions' log level to INFO; DEBUG messages will be omitted
|
|
||||||
logging.getLogger('transitions').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
class State(Enum):
|
class State(Enum):
|
||||||
PoweredOff = "powered off"
|
PoweredOff = "powered off"
|
||||||
@ -166,5 +163,3 @@ model = Overclocking()
|
|||||||
machine = MyMachine(model, states=State, initial=model.state)
|
machine = MyMachine(model, states=State, initial=model.state)
|
||||||
|
|
||||||
machine.get_graph().draw('my_state_diagram.svg', prog='dot')
|
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;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-option {
|
||||||
|
display:none;
|
||||||
}
|
}
|
@ -1,8 +1,10 @@
|
|||||||
|
let code_mirror;
|
||||||
|
|
||||||
function codemirror_load()
|
function codemirror_load()
|
||||||
{
|
{
|
||||||
const code_div = document.getElementById('codemirror');
|
const code_div = document.getElementById('codemirror');
|
||||||
|
|
||||||
var code_mirror = CodeMirror(code_div, {
|
code_mirror = CodeMirror(code_div, {
|
||||||
lineNumbers: true
|
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 row_headers = []
|
||||||
let col_headers = []
|
let col_headers = []
|
||||||
|
|
||||||
|
container.innerText = ""
|
||||||
|
|
||||||
Object.keys(data).forEach((key, index) => {
|
Object.keys(data).forEach((key, index) => {
|
||||||
row_headers.push(key)
|
row_headers.push(key)
|
||||||
Object.keys(data[key]).forEach((value, subIndex) => {
|
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') }}">
|
<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/vendor/handsontable/handsontable.full.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/table.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/profiles.js') }}"></script>
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
@ -24,26 +25,28 @@
|
|||||||
<div class="row-flex-container full-screen">
|
<div class="row-flex-container full-screen">
|
||||||
<div class="column-flex-container left-third">
|
<div class="column-flex-container left-third">
|
||||||
<div class="menubar">
|
<div class="menubar">
|
||||||
<!-- <div class="dropdown">
|
<select id="profile-select">
|
||||||
<button type="button">Load profile...</button>
|
<option disabled selected class="first-option">Load profile...</option>
|
||||||
<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>
|
</select>
|
||||||
<button type="button" onclick="socket.emit(`save_profile`);">Save</button>
|
<select id="serial-select" onchange="code_mirror.setValue(update_toml('esp32_serial', this.value));"">
|
||||||
<button type="button" onclick="socket.emit(`save_new_profile`);">Save as</button>
|
<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>
|
||||||
<div class="cm-editor">
|
<div class="cm-editor">
|
||||||
<div id="codemirror" class="cm-scroller"></div>
|
<div id="codemirror" class="cm-scroller"></div>
|
||||||
</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>
|
||||||
<div class="right-third">
|
<div class="right-third">
|
||||||
<div class="row-flex-container">
|
<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
|
import networkx as nx
|
||||||
from networkx import Graph
|
from networkx import Graph
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from ipkvm import esp32_serial
|
from ipkvm.util import esp32_serial
|
||||||
from ipkvm.util.mkb import ASCII2JS
|
from ipkvm.util.mkb import ASCII2JS
|
||||||
import time
|
import time
|
||||||
import tomlkit
|
|
||||||
|
|
||||||
# Type checker lunacy!
|
# Type checker lunacy!
|
||||||
type MultiDiGraph = Graph[Any]
|
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
|
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 = nx.shortest_path(graph, node_a, node_b)
|
||||||
path_edges = list(zip(path[:-1], path[1:]))
|
path_edges = list(zip(path[:-1], path[1:]))
|
||||||
edge_path= [(u, v, graph[u][v]) for u, v in path_edges]
|
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")
|
graph: MultiDiGraph = nx.nx_agraph.read_dot("bios-maps/asrock/b650e-riptide-wifi.gv")
|
||||||
|
|
||||||
current_node = "Main"
|
current_node = "Main"
|
||||||
@ -70,6 +65,6 @@ def test_route():
|
|||||||
for category in settings:
|
for category in settings:
|
||||||
for setting_node in settings[category]:
|
for setting_node in settings[category]:
|
||||||
if graph.nodes[setting_node]["value"] != settings[category][setting_node]:
|
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
|
current_node = setting_node
|
||||||
apply_setting(graph, setting_node, settings[category][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 = {
|
PoweredUpCodeDef = {
|
||||||
1: "System is entering S1 sleep state",
|
1: "System is entering S1 sleep state",
|
||||||
2: "System is entering S2 sleep state",
|
2: "System is entering S2 sleep state",
|
||||||
@ -543,245 +530,3 @@ POSTHex7Segment = {
|
|||||||
254: "FE",
|
254: "FE",
|
||||||
255: "FF"
|
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__':
|
if __name__ == '__main__':
|
||||||
ui.run(app, host='0.0.0.0', port=5000)
|
ui.run(app, host='0.0.0.0', port=5000)
|
||||||
#pass
|
|
@ -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