From 4e2036b1ccf1955d9b11fcd8c3cf6d466d92445e Mon Sep 17 00:00:00 2001 From: rawhide kobayashi Date: Sun, 23 Mar 2025 17:55:12 -0500 Subject: [PATCH] first automation steps functioning --- my_state_diagram.svg | 289 +++++++++++++++-------------- profiles/default.toml | 6 +- webui/ipkvm/app.py | 3 +- webui/ipkvm/states/events.py | 4 + webui/ipkvm/states/ssh.py | 69 +++++++ webui/ipkvm/states/states.py | 116 +++++++++--- webui/ipkvm/templates/index.html | 1 + webui/ipkvm/util/hwinfo/hwinfo.py | 99 ++++++---- webui/ipkvm/util/types/__init__.py | 1 + webui/ipkvm/util/video/video.py | 4 +- 10 files changed, 380 insertions(+), 212 deletions(-) create mode 100644 webui/ipkvm/states/ssh.py diff --git a/my_state_diagram.svg b/my_state_diagram.svg index bd594e2..fae17c9 100644 --- a/my_state_diagram.svg +++ b/my_state_diagram.svg @@ -4,265 +4,272 @@ - + - -State Machine + +State Machine PoweredOff - -PoweredOff + +PoweredOff + + + +PoweredOff->PoweredOff + + +hard_shutdown POST - -POST + +POST - + PoweredOff->POST - - -power_on + + +power_on - + POST->PoweredOff - - -hard_shutdown + + +hard_shutdown EnterBIOS - -EnterBIOS + +EnterBIOS - + POST->EnterBIOS - - -enter_bios + + +enter_bios IdleWaitingForInput - - -IdleWaitingForInput + + +IdleWaitingForInput - + POST->IdleWaitingForInput - - -go_idle + + +go_idle WaitingForOS - -WaitingForOS + +WaitingForOS - + POST->WaitingForOS - - -wait_os + + +wait_os BootLoop - -BootLoop + +BootLoop - + POST->BootLoop - - -unsuccessful_post + + +unsuccessful_post BIOSSetup - -BIOSSetup + +BIOSSetup - + EnterBIOS->BIOSSetup - - -start_bios_setup + + +start_bios_setup - + EnterBIOS->IdleWaitingForInput - - -go_idle + + +go_idle - + BIOSSetup->PoweredOff - - -hard_shutdown + + +hard_shutdown - + BIOSSetup->POST - - -finished_bios_setup + + +finished_bios_setup - + IdleWaitingForInput->PoweredOff - - -hard_shutdown | soft_shutdown + + +hard_shutdown | soft_shutdown - + IdleWaitingForInput->POST - - -reboot + + +reboot WaitingForHWInfo - -WaitingForHWInfo + +WaitingForHWInfo - + IdleWaitingForInput->WaitingForHWInfo - - -begin_automation + + +begin_automation - + WaitingForOS->PoweredOff - - -hard_shutdown + + +hard_shutdown - + WaitingForOS->WaitingForHWInfo - - -os_booted + + +os_booted OCTypeDecision - -OCTypeDecision + +OCTypeDecision - + WaitingForHWInfo->OCTypeDecision - - -hwinfo_available + + +hwinfo_available - + -RoughMulticoreUndervolt - -RoughMulticoreUndervolt +SynchronizeMulticoreVID + +SynchronizeMulticoreVID - - -OCTypeDecision->RoughMulticoreUndervolt - - -rough_multicore_undervolt + + +OCTypeDecision->SynchronizeMulticoreVID + + +synchronize_vid PreciseMulticoreUndervolt - -PreciseMulticoreUndervolt + +PreciseMulticoreUndervolt - + OCTypeDecision->PreciseMulticoreUndervolt - - -precise_multicore_undervolt + + +precise_multicore_undervolt SingleCoreTuning - -SingleCoreTuning + +SingleCoreTuning - -OCTypeDecision->SingleCoreTuning - - -single_core_tuning - - -RoughMulticoreUndervolt->PoweredOff - - -hard_shutdown +OCTypeDecision->SingleCoreTuning + + +single_core_tuning - + -RoughMulticoreUndervolt->POST - - -reboot +SynchronizeMulticoreVID->PoweredOff + + +hard_shutdown + + + +SynchronizeMulticoreVID->POST + + +reboot - + PreciseMulticoreUndervolt->PoweredOff - - -hard_shutdown + + +hard_shutdown - + PreciseMulticoreUndervolt->POST - - -reboot + + +reboot - + SingleCoreTuning->PoweredOff - - -hard_shutdown + + +hard_shutdown - + SingleCoreTuning->POST - - -reboot + + +reboot - + BootLoop->PoweredOff - - -trigger_cmos_reset + + +trigger_cmos_reset diff --git a/profiles/default.toml b/profiles/default.toml index b9ba39e..d4c73f8 100644 --- a/profiles/default.toml +++ b/profiles/default.toml @@ -10,9 +10,11 @@ fps = "60.000" [client] hostname = "10.20.30.90" hwinfo_port = "60000" # Unless you've changed it! +ssh_username = "rawhide" # MAKE SURE YOU USE SINGLE QUOTES, THIS HAS TO BE A LITERAL -ryzen_smu_cli_path = 'C:\Users\rawhide\Downloads\win-x64\ryzen-smu-cli.exe' -ycruncher_path = 'C:\Users\rawhide\Downloads\y-cruncher v0.8.6.9545b\y-cruncher v0.8.6.9545\y-cruncher.exe' +# Alternatively, just make sure ycruncher and ryzen-smu-cli are in your PATH and leave it as it is +ryzen_smu_cli_path = 'C:\Users\rawhide\Downloads\ryzen-smu-cli-0.0.2\ryzen-smu-cli.exe' +ycruncher_path = 'C:\Users\rawhide\Downloads\y-cruncher v0.8.6.9545\y-cruncher v0.8.6.9545\y-cruncher.exe' bios_map_path = 'bios-maps/asus/x870-gaming-plus-wifi.gv' [client.overclocking.common] # Just some ideas of how the settings may work for your system diff --git a/webui/ipkvm/app.py b/webui/ipkvm/app.py index a691bf4..f444e5c 100644 --- a/webui/ipkvm/app.py +++ b/webui/ipkvm/app.py @@ -5,4 +5,5 @@ import logging app = Flask(__name__) ui = SocketIO(app) logger = app.logger -logger.setLevel(logging.INFO) \ No newline at end of file +logger.setLevel(logging.INFO) +logger.propagate = False \ No newline at end of file diff --git a/webui/ipkvm/states/events.py b/webui/ipkvm/states/events.py index cea103c..7e45cbc 100644 --- a/webui/ipkvm/states/events.py +++ b/webui/ipkvm/states/events.py @@ -45,3 +45,7 @@ def handle_clear_cmos(): "pwr": GPIO.LOW.value } esp32_serial.mkb_queue.put(msg) + +@ui.on("begin_automation") +def handle_begin_automation(): + model.begin_automation() diff --git a/webui/ipkvm/states/ssh.py b/webui/ipkvm/states/ssh.py new file mode 100644 index 0000000..656ae2c --- /dev/null +++ b/webui/ipkvm/states/ssh.py @@ -0,0 +1,69 @@ +import paramiko +from paramiko import Channel + +class ssh_conn(): + def __init__(self, hostname: str, ssh_username: str, ycruncher_path: str, ryzen_smu_cli_path: str): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Yuck! + self.ycruncher_cmd = f"& \"{ycruncher_path.replace("\\", "\\\\")}\"\r" + # MEGA YUCK! + self.smu_cmd = f"Start-Process -FilePath \"{ryzen_smu_cli_path.replace("\\", "\\\\")}\" -Verb RunAs -ArgumentList " + + client.connect(hostname=hostname, username=ssh_username, key_filename=f"/home/{ssh_username}/.ssh/id_rsa.pub") + + self.channel_ycruncher = client.invoke_shell() + self.channel_smu = client.invoke_shell() + + def ez_send_line(self, channel: Channel, cmd: str | int, wait: bool = False, wait_string: str = ""): + channel.send(f"{cmd}\r".encode()) + + if wait: + self.read_channel_line_until(channel, wait_string) + + def read_channel_line_until(self, channel: Channel, desired_line: str): + line = "" + for byte in iter(lambda: channel.recv(1), b""): + line += byte.decode() + if desired_line.lower() in line.lower(): + return + elif byte == b'\n' or byte == b'\r': + line = "" + + def start_ycruncher(self, core_list: list[int], test_list: list[int], test_length: int = 120): + self.ez_send_line(self.channel_ycruncher, self.ycruncher_cmd, True, "option:") + self.ez_send_line(self.channel_ycruncher, 2, True, "option:") + self.ez_send_line(self.channel_ycruncher, 1, True, "option:") + self.ez_send_line(self.channel_ycruncher, "d", True, "option:") + + for core in core_list: + self.ez_send_line(self.channel_ycruncher, core, True, "option:") + + self.ez_send_line(self.channel_ycruncher, "", True, "option:") + self.ez_send_line(self.channel_ycruncher, 8, True, "option:") + for test in test_list: + self.ez_send_line(self.channel_ycruncher, test, True, "option:") + + self.ez_send_line(self.channel_ycruncher, 4, True, "(seconds) =") + self.ez_send_line(self.channel_ycruncher, test_length, True, "option:") + + self.ez_send_line(self.channel_ycruncher, 0, False) + + def run_smu_cmd(self, args: str): + self.ez_send_line(self.channel_smu, f"{self.smu_cmd}\"{args}\"") + + + + #start_ycruncher(channel, + # "& \"C:\\Users\\rawhide\\Downloads\\y-cruncher v0.8.6.9545\\y-cruncher v0.8.6.9545\\y-cruncher.exe\"\r", + # [0,1,6,9,19,25,31], [11,12], 5) +# + #while True: + # line = b'' + # for byte in iter(lambda: channel.recv(1), b""): + # line += byte + # if byte == b'\n' or byte == b'\r': + # line = f"{line.decode('utf-8', errors='replace').rstrip()}" + # print(line) + # line = b'' diff --git a/webui/ipkvm/states/states.py b/webui/ipkvm/states/states.py index 452d98c..06c13d2 100644 --- a/webui/ipkvm/states/states.py +++ b/webui/ipkvm/states/states.py @@ -5,7 +5,10 @@ from transitions.extensions import GraphMachine from ipkvm.util.mkb import esp32_serial from ipkvm.util.mkb.mkb import GPIO from ipkvm.util.mkb.scancodes import HIDKeyCode -from ipkvm.app import logging, ui +from ipkvm.app import logging, ui, logger +from ipkvm.util.hwinfo import hw_monitor +from ipkvm.states import ssh +from ipkvm.util.profiles import profile_manager logging.basicConfig(level=logging.INFO) @@ -15,7 +18,7 @@ class State(Enum): BIOSSetup = "bios setup" WaitingForOS = "waiting for os" OCTypeDecision = "next process decision" - RoughMulticoreUndervolt = "rough multicore undervolting" + SynchronizeMulticoreVID = "sync multicore vid" PreciseMulticoreUndervolt = "precise multicore undervolting" POST = "power on self test" WaitingForHWInfo = "waiting for hwinfo" @@ -37,6 +40,8 @@ class Overclocking: _current_BIOS_location = "EZ Mode" + _PBO_offsets: list[int] = [] + # TRANSITION DEFINITIONS @add_transitions(transition(State.PoweredOff, State.POST, unless="client_powered")) def power_on(self): ... @@ -68,21 +73,20 @@ class Overclocking: @add_transitions(transition(State.BootLoop, State.PoweredOff)) def trigger_cmos_reset(self): ... - @add_transitions(transition([State.IdleWaitingForInput, State.RoughMulticoreUndervolt, + @add_transitions(transition([State.IdleWaitingForInput, State.SynchronizeMulticoreVID, State.PreciseMulticoreUndervolt, State.SingleCoreTuning], State.POST)) def reboot(self): ... @add_transitions(transition([State.BIOSSetup, State.IdleWaitingForInput, State.POST, State.WaitingForOS, - State.RoughMulticoreUndervolt, State.PreciseMulticoreUndervolt, - State.SingleCoreTuning], State.PoweredOff, after="_hard_shutdown")) + State.SynchronizeMulticoreVID, State.PreciseMulticoreUndervolt, + State.SingleCoreTuning, State.PoweredOff], State.PoweredOff, after="_hard_shutdown")) def hard_shutdown(self): ... - @add_transitions(transition(State.IdleWaitingForInput, State.PoweredOff, - after="_soft_shutdown")) + @add_transitions(transition(State.IdleWaitingForInput, State.PoweredOff, after="_soft_shutdown")) def soft_shutdown(self): ... - @add_transitions(transition(State.OCTypeDecision, State.RoughMulticoreUndervolt)) - def rough_multicore_undervolt(self): ... + @add_transitions(transition(State.OCTypeDecision, State.SynchronizeMulticoreVID)) + def synchronize_vid(self): ... @add_transitions(transition(State.OCTypeDecision, State.PreciseMulticoreUndervolt)) def precise_multicore_undervolt(self): ... @@ -93,6 +97,13 @@ class Overclocking: @add_transitions(transition([State.POST, State.EnterBIOS], State.IdleWaitingForInput)) def go_idle(self): ... + # I WANTED THESE TO BE FUNCTION REFERENCES BUT IT DOESN'T WORK + _OC_steps = { + "synchronize_vid": False, + "precise_multicore_undervolt": False, + "single_core_tuning": False + } + # PROPERTIES GO HERE @property @@ -133,20 +144,9 @@ class Overclocking: self.go_idle() def on_enter_EnterBIOS(self): - # # Wait until the POST has progressed far enough for USB devices to be loaded and options to be imminent - # esp32_serial.notify_code = "45" - # esp32_serial.active_notification_request.set() - # esp32_serial.post_code_notify.wait() - # esp32_serial.post_code_notify.clear() -# - # # Spam delete until the BIOS is loaded - # esp32_serial.notify_code = "Ab" - # esp32_serial.active_notification_request.set() - # while not esp32_serial.post_code_notify.is_set(): - spam_timer = time.time() - # Crushed by my lack of consistent access to BIOS post codes, we simply take our time... + # Crushed by my lack of consistent access to BIOS post codes, we simply spam once USB is available... while time.time() - spam_timer <= 10: msg = { "key_down": HIDKeyCode.Delete.value @@ -159,9 +159,7 @@ class Overclocking: esp32_serial.mkb_queue.put(msg) time.sleep(0.1) - # esp32_serial.post_code_notify.clear() - - # Wait a few seconds for the BIOS to become responsive + # Wait a few seconds for the BIOS to become responsive... time.sleep(5) self._enter_bios_flag = False @@ -172,9 +170,77 @@ class Overclocking: else: self.go_idle() + def on_enter_WaitingForHWInfo(self): + self._running_automatic = True + while not hw_monitor.is_hwinfo_alive: + pass + + self.hwinfo_available() + + def on_enter_OCTypeDecision(self): + # I WANTED THESE TO BE FUNCTION REFERENCES BUT IT DOESN'T WORK + if not self._OC_steps["synchronize_vid"]: + self.synchronize_vid() + + elif not self._OC_steps["precise_multicore_undervolt"]: + self.precise_multicore_undervolt() + + elif not self._OC_steps["single_core_tuning"]: + self.single_core_tuning() + + def on_enter_SynchronizeMulticoreVID(self): + ssh_conn = ssh.ssh_conn(profile_manager.profile["client"]["hostname"], + profile_manager.profile["client"]["ssh_username"], + profile_manager.profile["client"]["ycruncher_path"], + profile_manager.profile["client"]["ryzen_smu_cli_path"]) + + self._PBO_offsets = [0] * hw_monitor.core_dataframe.shape[0] + + ssh_conn.start_ycruncher([x for x in range(hw_monitor.core_dataframe.shape[0] * 2)], [11]) + + conditions_met = False + + while not conditions_met: + avg_vid: dict[str, float] = {f"Core {x}": 0 for x in range(hw_monitor.core_dataframe.shape[0])} + + # Short wait to allow values to settle. This could be expanded to wait until the PPT does not change + # within a certain envelope for worse cooling systems... + time.sleep(5) + + data = hw_monitor.collect_data(10) + + + + for frame in data: + for row in frame.itertuples(): + avg_vid[row.Index] = avg_vid[row.Index] + row.VID + + data_len = len(data) + + for core in avg_vid: + avg_vid[core] = avg_vid[core] / data_len + + highest_vid_index = self.get_highest_vid_index(avg_vid) + self._PBO_offsets[highest_vid_index] -= 1 + ssh_conn.run_smu_cmd(f"--offset {highest_vid_index}:{self._PBO_offsets[highest_vid_index]}") + logger.info(f"Highest core was core {highest_vid_index} at {avg_vid[f"Core {highest_vid_index}"]}, changed offset to {self._PBO_offsets[highest_vid_index]}!") + + print(self._PBO_offsets) + + def get_highest_vid_index(self, vid_list: dict[str, float]): + highest_index = 0 + for index in range(len(vid_list) - 1): + if vid_list[f"Core {index + 1}"] > vid_list[f"Core {highest_index}"]: + highest_index = index + 1 + + return highest_index + + + + # STATE EXIT FUNCTIONS def on_exit_PoweredOff(self): - self._power_switch(0.2) + self._power_switch(0.2) # UTILITY FUNCTIONS GO HERE def _power_switch(self, delay: float): diff --git a/webui/ipkvm/templates/index.html b/webui/ipkvm/templates/index.html index 0af0a89..fbd1cce 100644 --- a/webui/ipkvm/templates/index.html +++ b/webui/ipkvm/templates/index.html @@ -42,6 +42,7 @@ +
diff --git a/webui/ipkvm/util/hwinfo/hwinfo.py b/webui/ipkvm/util/hwinfo/hwinfo.py index 48199bb..75dc094 100644 --- a/webui/ipkvm/util/hwinfo/hwinfo.py +++ b/webui/ipkvm/util/hwinfo/hwinfo.py @@ -10,7 +10,9 @@ 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.core_dataframe: pd.DataFrame + self._is_hwinfo_alive = False + self._new_data = threading.Event() self.start() def run(self): @@ -21,62 +23,77 @@ class HWInfoMonitor(threading.Thread): def do_work(self): - self.create_dataframe() - time.sleep(0.25) + while not profile_manager.restart_hwinfo.is_set(): + self.create_dataframe() + time.sleep(0.25) def create_dataframe(self): try: request = requests.get(self.request_url, timeout=1) - data = request.json() + if request.status_code == 200 and len(request.json()) > 0: + self._is_hwinfo_alive = True + 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] = [] + 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"] + for reading in data["hwinfo"]["readings"]: + label = reading["labelOriginal"] - match = re.match(r"(?PCore[0-9]* \(CCD[0-9]\))|(?PCore [0-9]* VID)|(?PCore [0-9]* T0 Effective Clock)|(?PCore [0-9]* Power)", label) + match = re.match(r"(?PCore[0-9]* \(CCD[0-9]\))|(?PCore [0-9]* VID)|(?PCore [0-9]* T0 Effective Clock)|(?PCore [0-9]* Power)", label) - if match: - if match.group("core_ccd"): - core_ccd = match.group("core_ccd").split(' ') - core_ccd[0] = core_ccd[0][:4] + ' ' + core_ccd[0][4:] - cpu_list.append(core_ccd[0]) - ccd_list.append(core_ccd[1].strip('()')) - temp_list.append(round(reading["value"], 2)) + 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(round(reading["value"], 3)) + elif match.group("core_vid"): + vid_list.append(round(reading["value"], 3)) - elif match.group("core_mhz"): - mhz_list.append(round(reading["value"], 2)) + elif match.group("core_mhz"): + mhz_list.append(round(reading["value"], 2)) - elif match.group("core_power"): - power_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) + self.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")) + ui.emit("update_core_info_table", self.core_dataframe.to_dict("index")) + + self._new_data.set() + + else: + self._is_hwinfo_alive = False except Exception as e: - #print(e) + #logger.info(e) pass + + def collect_data(self, time_to_collect: int): + start_time = time.time() + dataframe_list: list[pd.DataFrame] = [] + while time.time() - start_time <= time_to_collect: + self._new_data.wait() + self._new_data.clear() + dataframe_list.append(self.core_dataframe) + + return dataframe_list + + @property 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 + return self._is_hwinfo_alive diff --git a/webui/ipkvm/util/types/__init__.py b/webui/ipkvm/util/types/__init__.py index c7bef7f..10090d7 100644 --- a/webui/ipkvm/util/types/__init__.py +++ b/webui/ipkvm/util/types/__init__.py @@ -21,6 +21,7 @@ class OverclockingDict(TypedDict): class ClientDict(TypedDict): hostname: str hwinfo_port: str + ssh_username: str ryzen_smu_cli_path: str ycruncher_path: str bios_map_path: str diff --git a/webui/ipkvm/util/video/video.py b/webui/ipkvm/util/video/video.py index f5ab90c..16a6e45 100644 --- a/webui/ipkvm/util/video/video.py +++ b/webui/ipkvm/util/video/video.py @@ -3,8 +3,8 @@ import dataclasses import re import subprocess import threading -import av -import av.container +#import av +#import av.container import cv2 from ipkvm.app import logger, ui from ipkvm.util.profiles import profile_manager