diff --git a/README.md b/README.md index 7f2c420..3c0fa7f 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,200 @@ -# Bitaxe Hashrate Benchmark +# **Bitaxe Hashrate Benchmark** A Python-based benchmarking tool for optimizing Bitaxe mining performance by testing different voltage and frequency combinations while monitoring hashrate, temperature, and power efficiency. -## Features +## **Features** -- Automated benchmarking of different voltage/frequency combinations -- Temperature monitoring and safety cutoffs -- Power efficiency calculations (J/TH) -- Automatic saving of benchmark results -- Graceful shutdown with best settings retention -- Docker support for easy deployment +* Automated benchmarking of different voltage/frequency combinations +* Direct setting of specific voltage and frequency from command line +* Temperature monitoring and safety cutoffs +* Power consumption monitoring and reporting (Watts) +* Fan speed monitoring and reporting (Percentage) +* Power efficiency calculations (J/TH) +* Automatic saving of benchmark results +* Graceful shutdown with best settings retention +* Docker support for easy deployment -## Prerequisites +## **Prerequisites** -- Python 3.11 or higher -- Access to a Bitaxe miner on your network -- Docker (optional, for containerized deployment) -- Git (optional, for cloning the repository) +* Python 3.11 or higher +* Access to a Bitaxe miner on your network +* Docker (optional, for containerized deployment) +* Git (optional, for cloning the repository) -## Installation +## **Installation** -### Standard Installation +### **Standard Installation** -1. Clone the repository: -```bash -git clone https://github.com/mrv777/Bitaxe-Hashrate-Benchmark.git -cd Bitaxe-Hashrate-Benchmark -``` +1. Clone the repository: + ```bash + git clone https://github.com/mrv777/Bitaxe-Hashrate-Benchmark.git + cd Bitaxe-Hashrate-Benchmark + ``` -2. Create and activate a virtual environment: -```bash -python -m venv venv -# On Windows -venv\Scripts\activate -# On Linux/Mac -source venv/bin/activate -``` +2. Create and activate a virtual environment: + ```bash + python -m venv venv + # On Windows + venv\Scripts\activate + # On Linux/Mac + source venv/bin/activate + ``` -3. Install dependencies: -```bash -pip install -r requirements.txt -``` +3. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +### **Docker Installation** + +1. Build the Docker image: + `docker build -t bitaxe-benchmark .` -### Docker Installation +## **Usage** -1. Build the Docker image: +### **Standard Usage (Run Full Benchmark)** + +Run the benchmark tool by providing your Bitaxe's IP address and initial settings: ```bash -docker build -t bitaxe-benchmark . +python bitaxe_hashrate_benchmark.py -v -f ``` -## Usage - -### Standard Usage +**Arguments:** -Run the benchmark tool by providing your Bitaxe's IP address: +* ``: **Required.** IP address of your Bitaxe miner (e.g., `192.168.2.26`). +* `-v, --voltage:` **Optional.** Initial voltage in mV for testing (default: `1150`). +* `-f, --frequency:` **Optional.** Initial frequency in MHz for testing (default: `500`). +**Example:** ```bash -python bitaxe_hashrate_benchmark.py +python bitaxe_hashrate_benchmark.py 192.168.1.136 -v 1150 -f 550 ``` -Optional parameters: -- `-v, --voltage`: Initial voltage in mV (default: 1150) -- `-f, --frequency`: Initial frequency in MHz (default: 500) +### **Apply Specific Settings (Without Benchmarking)** -Example: +To quickly apply specific voltage and frequency settings to your Bitaxe without running the full benchmark: ```bash -python bitaxe_hashrate_benchmark.py 192.168.2.29 -v 1175 -f 775 +python bitaxe_hashrate_benchmark.py --set-values -v -f ``` -### Docker Usage (Optional) +**Arguments:** -Run the container with your Bitaxe's IP address: +* ``: **Required.** IP address of your Bitaxe miner. +* `-s, --set-values`: **Flag.** Activates this mode to only set values and exit. +* `-v, --voltage`: **Required.** The exact voltage in mV to apply. +* `-f, --frequency`: **Required.** The exact frequency in MHz to apply. +**Example:** +```bash +python bitaxe_hashrate_benchmark.py 192.168.1.136 --set-values -v 1150 -f 780 +``` + +### **Docker Usage (Optional)** + +Run the container with your Bitaxe's IP address (add --set-values for that mode): ```bash docker run --rm bitaxe-benchmark [options] ``` -Example: +Example (Full Benchmark): ```bash docker run --rm bitaxe-benchmark 192.168.2.26 -v 1200 -f 550 ``` -## Configuration - -The script includes several configurable parameters: - -- Maximum chip temperature: 66°C -- Maximum VR temperature: 86°C -- Maximum allowed voltage: 1400mV -- Minimum allowed voltage: 1000mV -- Maximum allowed frequency: 1200MHz -- Maximum power consumption: 40W -- Minimum allowed frequency: 400MHz -- Minimum input voltage: 4800mV -- Maximum input voltage: 5500mV -- Benchmark duration: 10 minutes -- Sample interval: 15 seconds -- Sleep time before benchmark: 90 seconds -- **Minimum required samples: 7** (for valid data processing) -- Voltage increment: 20mV -- Frequency increment: 25MHz - -## Output - -The benchmark results are saved to `bitaxe_benchmark_results_.json`, containing: -- Complete test results for all combinations -- Top 5 performing configurations ranked by hashrate -- Top 5 most efficient configurations ranked by J/TH -- For each configuration: - - Average hashrate (with outlier removal) - - Temperature readings (excluding initial warmup period) - - VR temperature readings (when available) - - Power efficiency metrics (J/TH) - - Input voltage measurements - - Voltage/frequency combinations tested - -## Safety Features - -- Automatic temperature monitoring with safety cutoff (66°C chip temp) -- Voltage regulator (VR) temperature monitoring with safety cutoff (86°C) -- Input voltage monitoring with minimum threshold (4800mV) and maximum threshold (5500mV) -- Power consumption monitoring with safety cutoff (40W) -- Temperature validation (must be above 5°C) -- Graceful shutdown on interruption (Ctrl+C) -- Automatic reset to best performing settings after benchmarking -- Input validation for safe voltage and frequency ranges -- Hashrate validation to ensure stability -- Protection against invalid system data -- Outlier removal from benchmark results - -## Benchmarking Process +Example (Set Settings Only): +```bash +docker run --rm bitaxe-benchmark 192.168.2.26 --set-values -v 1150 -f 780 +``` + +## **Configuration** + +The script includes several configurable parameters. These can be adjusted in the bitaxe_hashrate_benchmark.py file: + +* Maximum chip temperature: 66°C +* Maximum VR temperature: 86°C +* Maximum allowed voltage: 1400mV +* Minimum allowed voltage: 1000mV +* Maximum allowed frequency: 1200MHz +* Maximum power consumption: 30W +* Minimum allowed frequency: 400MHz +* Minimum input voltage: 4800mV +* Maximum input voltage: 5500mV +* Benchmark duration: 600 seconds (10 minutes per combination) +* Sample interval: 15 seconds +* Sleep time before benchmark: 90 seconds +* Minimum required samples: 7 (for valid data processing) +* Voltage increment: 15mV +* Frequency increment: 20MHz +* ASIC Configuration: asic_count is hardcoded to 1 as it's not always provided by the API. small_core_count is fetched from the Bitaxe. + +## **Output** + +The benchmark results are saved to bitaxe_benchmark_results_.json, containing: + +* Complete test results for all combinations +* Top 5 performing configurations ranked by hashrate +* Top 5 most efficient configurations ranked by J/TH +* For each configuration: + * Average hashrate (with outlier removal) + * Temperature readings (excluding initial warmup period) + * VR temperature readings (when available) + * Power efficiency metrics (J/TH) + * Average Power (Watts) + * Average Fan Speed (Percentage or RPM, if available from API) + * Input voltage measurements + * Voltage/frequency combinations tested + * Error reason (if any) for a specific iteration + +## **Safety Features** + +* Automatic temperature monitoring with safety cutoff (66°C chip temp) +* Voltage regulator (VR) temperature monitoring with safety cutoff (86°C) +* Input voltage monitoring with minimum threshold (4800mV) and maximum threshold (5500mV) +* Power consumption monitoring with safety cutoff (30W) +* Temperature validation (must be above 5°C) +* Graceful shutdown on interruption (Ctrl+C) +* Automatic reset to best performing settings after benchmarking +* Input validation for safe voltage and frequency ranges +* Hashrate validation to ensure stability +* Protection against invalid system data +* Outlier removal from benchmark results + +## **Benchmarking Process** The tool follows this process: -1. Starts with user-specified or default voltage/frequency -2. Tests each combination for 20 minutes -3. Validates hashrate is within 8% of theoretical maximum -4. Incrementally adjusts settings: - - Increases frequency if stable - - Increases voltage if unstable - - Stops at thermal or stability limits -5. Records and ranks all successful configurations -6. Automatically applies the best performing stable settings -7. Restarts system after each test for stability + +1. Starts with user-specified or default voltage/frequency +2. Tests each combination for 10 minutes +3. Validates hashrate is within 6% of theoretical maximum +4. Incrementally adjusts settings: + * Increases frequency if stable + * Increases voltage if unstable + * Stops at thermal or stability limits +5. Records and ranks all successful configurations +6. Automatically applies the best performing stable settings +7. Restarts system after each test for stability 8. Allows 90-second stabilization period between tests -## Data Processing +## **Data Processing** The tool implements several data processing techniques to ensure accurate results: -- Removes 3 highest and 3 lowest hashrate readings to eliminate outliers -- Excludes first 6 temperature readings during warmup period -- Validates hashrate is within 6% of theoretical maximum -- Averages power consumption across entire test period -- Monitors VR temperature when available -- Calculates efficiency in Joules per Terahash (J/TH) -## Contributing +* Removes 3 highest and 3 lowest hashrate readings to eliminate outliers +* Excludes first 6 temperature readings during warmup period +* Validates hashrate is within 6% of theoretical maximum +* Averages power consumption across entire test period +* Monitors VR temperature when available +* Calculates efficiency in Joules per Terahash (J/TH) +* Averages fan speed across entire test period + +## **Contributing** Contributions are welcome! Please feel free to submit a Pull Request. -## License +## **License** This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. -## Disclaimer +## **Disclaimer** Please use this tool responsibly. Overclocking and voltage modifications can potentially damage your hardware if not done carefully. Always ensure proper cooling and monitor your device during benchmarking. diff --git a/bitaxe_hashrate_benchmark.py b/bitaxe_hashrate_benchmark.py index b2f454b..3c2303f 100644 --- a/bitaxe_hashrate_benchmark.py +++ b/bitaxe_hashrate_benchmark.py @@ -4,12 +4,22 @@ import signal import sys import argparse +import statistics from datetime import datetime START_TIME = datetime.now().strftime("%Y-%m-%d_%H") +# Enable ansi escape characters in terminal - colors were not working in Windows terminal +import os +try: + import colorama + colorama.init() +except ImportError: + # Fallback for environments where colorama isn't available + if os.name == "nt": + os.system("") # rudimentary ANSI enable on Windows -if 'START_TIME' not in globals(): - START_TIME = datetime.now().strftime("%Y-%m-%d_%H") +# Compute timestamp for file suffix +timestamp = time.strftime("%Y%m%d-%H%M%S") # ANSI Color Codes GREEN = "\033[92m" @@ -17,45 +27,127 @@ RED = "\033[91m" RESET = "\033[0m" -# Add this before the configuration section +# This formatter allows for multi-line descriptions in help messages and adds default values +class RawTextAndDefaultsHelpFormatter(argparse.RawTextHelpFormatter): + def _get_help_string(self, action): + help_text = super()._get_help_string(action) + if action.default is not argparse.SUPPRESS: + # Append default value to help text if available + defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] + if action.option_strings or action.nargs in defaulting_nargs: + if "\n" in help_text: + help_text += f"\n(default: {action.default})" + else: + help_text += f" (default: {action.default})" + return help_text + +# Modify the parse_arguments function def parse_arguments(): - parser = argparse.ArgumentParser(description='Bitaxe Hashrate Benchmark Tool') - parser.add_argument('bitaxe_ip', nargs='?', help='IP address of the Bitaxe (e.g., 192.168.2.26)') - parser.add_argument('-v', '--voltage', type=int, default=1150, - help='Initial voltage in mV (default: 1150)') - parser.add_argument('-f', '--frequency', type=int, default=500, - help='Initial frequency in MHz (default: 500)') - + parser = argparse.ArgumentParser( + description= + f"{GREEN}Bitaxe Hashrate Benchmark Tool v1.0{RESET}\n" + "This script allows you to either benchmark your Bitaxe miner across various " + "voltage and frequency settings, or apply specific settings directly.\n", + epilog= + f"{YELLOW}Examples:{RESET}\n" + f" {YELLOW}1. Run a full benchmark (starting at 1150mV, 500MHz):{RESET}\n" + f" {GREEN}python bitaxe_hasrate_benchmark.py 192.168.1.136 -v 1150 -f 500{RESET}\n\n" + f" {YELLOW}2. Apply specific settings (1150mV, 780MHz) and exit:{RESET}\n" + f" {GREEN}python bitaxe_hasrate_benchmark.py 192.168.1.136 --set-values -v 1150 -f 780{RESET}\n\n" + f" {YELLOW}3. Get help (this message):{RESET}\n" + f" {GREEN}python bitaxe_hasrate_benchmark.py --help{RESET}", + formatter_class=RawTextAndDefaultsHelpFormatter # <--- USE THE CUSTOM FORMATTER + ) + + # Positional Argument + parser.add_argument( + 'bitaxe_ip', + nargs='?', # Makes it optional if --help is used alone, but required otherwise + help=f"{YELLOW}IP address of your Bitaxe miner (e.g., 192.168.2.26){RESET}\n" + " This is required for both benchmarking and setting values." + ) + + # Optional Arguments + parser.add_argument( + '-v', '--voltage', + type=int, + default=None, # Default is None to allow validation logic + help=f"{YELLOW}Core voltage in mV.{RESET}\n" + " For benchmark mode: The starting voltage for testing (default: 1150).\n" + " For --set-values mode: The exact voltage to apply (Required)." + ) + parser.add_argument( + '-f', '--frequency', + type=int, + default=None, # Default is None to allow validation logic + help=f"{YELLOW}Core frequency in MHz.{RESET}\n" + " For benchmark mode: The starting frequency for testing (default: 500).\n" + " For --set-values mode: The exact frequency to apply (Required)." + ) + + # New argument for setting values only + parser.add_argument( + '-s', '--set-values', + action='store_true', + help=f"{YELLOW}Set values only; do not run benchmark.{RESET}\n" + " If this flag is present, the script will apply the voltage (-v) and\n" + " frequency (-f) settings to the Bitaxe and then exit." + ) + + parser.add_argument( + '--max-temp', + type=int, + default=66, + help=f"{YELLOW}Maximum chip temperature in °C (default: 66).{RESET}\n" + " The benchmark will stop if this temperature is exceeded." + ) + # If no arguments are provided, print help and exit if len(sys.argv) == 1: parser.print_help() sys.exit(1) - + return parser.parse_args() # Replace the configuration section args = parse_arguments() + +# Validation for bitaxe_ip +if args.bitaxe_ip is None: + print(RED + "Error: Bitaxe IP address is required." + RESET) + sys.exit(1) + bitaxe_ip = f"http://{args.bitaxe_ip}" -initial_voltage = args.voltage -initial_frequency = args.frequency + +# Logic for voltage and frequency defaults/requirements +if args.set_values: + if args.voltage is None or args.frequency is None: + print(RED + "Error: --set-values mode requires both -v/--voltage and -f/--frequency." + RESET) + sys.exit(1) + initial_voltage = args.voltage + initial_frequency = args.frequency +else: + # Benchmark mode defaults + initial_voltage = args.voltage if args.voltage is not None else 1150 + initial_frequency = args.frequency if args.frequency is not None else 500 # Configuration -voltage_increment = 20 -frequency_increment = 25 +voltage_increment = 15 +frequency_increment = 20 sleep_time = 90 # Wait 90 seconds before starting the benchmark benchmark_time = 600 # 10 minutes benchmark time sample_interval = 15 # 15 seconds sample interval -max_temp = 66 # Will stop if temperature reaches or exceeds this value +max_temp = args.max_temp # Will stop if temperature reaches or exceeds this value max_allowed_voltage = 1400 # Maximum allowed core voltage max_allowed_frequency = 1200 # Maximum allowed core frequency max_vr_temp = 86 # Maximum allowed voltage regulator temperature min_input_voltage = 4800 # Minimum allowed input voltage max_input_voltage = 5500 # Maximum allowed input voltage -max_power = 40 # Max of 40W because of DC plug +max_power = 30 # Max of 30W because of DC plug # Add these variables to the global configuration section small_core_count = None -asic_count = None +asic_count = 1 # Add these constants to the configuration section min_allowed_voltage = 1000 # Minimum allowed core voltage @@ -79,6 +171,19 @@ def parse_arguments(): if benchmark_time / sample_interval < 7: raise ValueError(RED + f"Error: Benchmark time is too short. Please increase the benchmark time or decrease the sample interval. At least 7 samples are required." + RESET) +# Add suffix to filename in case of manual initial voltage/frequency +file_suffix = "" +if initial_voltage != 1150: + file_suffix = file_suffix + "_v" + str(initial_voltage) +if initial_frequency != 500: + file_suffix = file_suffix + "_f" + str(initial_frequency) + +# Refactor filename (called in multiple places) +def result_filename(): + # Extract IP from bitaxe_ip global variable and remove 'http://' + ip_address = bitaxe_ip.replace('http://', '') + return f"bitaxe_benchmark_results_{ip_address}_{timestamp}{file_suffix}.json" + # Results storage results = [] @@ -89,6 +194,13 @@ def parse_arguments(): # Check if we're handling an interrupt (Ctrl+C) handling_interrupt = False +def running_stddev(N, s1, s2): + if N > 1: + var = (N * s2 - s1 ** 2) / (N * (N - 1)) + return max(var, 0.0) ** 0.5 + else: + return 0.0 + def fetch_default_settings(): global default_voltage, default_frequency, small_core_count, asic_count @@ -207,9 +319,10 @@ def set_system_settings(core_voltage, frequency): response.raise_for_status() # Raise an exception for HTTP errors print(YELLOW + f"Applying settings: Voltage = {core_voltage}mV, Frequency = {frequency}MHz" + RESET) time.sleep(2) - restart_system() + return restart_system() except requests.exceptions.RequestException as e: print(RED + f"Error setting system settings: {e}" + RESET) + return False def restart_system(): try: @@ -222,21 +335,52 @@ def restart_system(): print(YELLOW + f"Applying new settings and waiting {sleep_time}s for system stabilization..." + RESET) response = requests.post(f"{bitaxe_ip}/api/system/restart", timeout=10) response.raise_for_status() # Raise an exception for HTTP errors - time.sleep(sleep_time) # Allow sleep_time for the system to restart and start hashing + + # Monitor system during stabilization period + start_wait = time.time() + while time.time() - start_wait < sleep_time: + try: + info = get_system_info() + if info: + temp = info.get("temp") + power = info.get("power") + vr_temp = info.get("vrTemp") + + if temp and temp >= max_temp: + print(RED + f"CRITICAL: Chip temperature {temp}°C exceeded limit {max_temp}°C during stabilization!" + RESET) + return False + + if vr_temp and vr_temp >= max_vr_temp: + print(RED + f"CRITICAL: VR temperature {vr_temp}°C exceeded limit {max_vr_temp}°C during stabilization!" + RESET) + return False + + if power and power > max_power: + print(RED + f"CRITICAL: Power {power}W exceeded limit {max_power}W during stabilization!" + RESET) + return False + + except Exception: + pass # Ignore transient errors during restart + time.sleep(5) else: print(YELLOW + "Applying final settings..." + RESET) response = requests.post(f"{bitaxe_ip}/api/system/restart", timeout=10) response.raise_for_status() # Raise an exception for HTTP errors + + return True except requests.exceptions.RequestException as e: print(RED + f"Error restarting the system: {e}" + RESET) + return False def benchmark_iteration(core_voltage, frequency): current_time = time.strftime("%H:%M:%S") print(GREEN + f"[{current_time}] Starting benchmark for Core Voltage: {core_voltage}mV, Frequency: {frequency}MHz" + RESET) hash_rates = [] + s1 = 0.0 + s2 = 0.0 temperatures = [] - power_consumptions = [] + power_consumptions = [] vr_temps = [] + fan_speeds = [] total_samples = benchmark_time // sample_interval expected_hashrate = frequency * ((small_core_count * asic_count) / 1000) # Calculate expected hashrate based on frequency @@ -244,66 +388,82 @@ def benchmark_iteration(core_voltage, frequency): info = get_system_info() if info is None: print(YELLOW + "Skipping this iteration due to failure in fetching system info." + RESET) - return None, None, None, False, None, "SYSTEM_INFO_FAILURE" + return None, None, None, None, False, None, None, None, "SYSTEM_INFO_FAILURE" temp = info.get("temp") vr_temp = info.get("vrTemp") # Get VR temperature if available voltage = info.get("voltage") if temp is None: print(YELLOW + "Temperature data not available." + RESET) - return None, None, None, False, None, "TEMPERATURE_DATA_FAILURE" + return None, None, None, None, False, None, None, None, "TEMPERATURE_DATA_FAILURE" if temp < 5: print(YELLOW + "Temperature is below 5°C. This is unexpected. Please check the system." + RESET) - return None, None, None, False, None, "TEMPERATURE_BELOW_5" + return None, None, None, None, False, None, None, None, "TEMPERATURE_BELOW_5" # Check both chip and VR temperatures if temp >= max_temp: print(RED + f"Chip temperature exceeded {max_temp}°C! Stopping current benchmark." + RESET) - return None, None, None, False, None, "CHIP_TEMP_EXCEEDED" + return None, None, None, None, False, None, None, None, "CHIP_TEMP_EXCEEDED" if vr_temp is not None and vr_temp >= max_vr_temp: print(RED + f"Voltage regulator temperature exceeded {max_vr_temp}°C! Stopping current benchmark." + RESET) - return None, None, None, False, None, "VR_TEMP_EXCEEDED" + return None, None, None, None, False, None, None, None, "VR_TEMP_EXCEEDED" if voltage < min_input_voltage: print(RED + f"Input voltage is below the minimum allowed value of {min_input_voltage}mV! Stopping current benchmark." + RESET) - return None, None, None, False, None, "INPUT_VOLTAGE_BELOW_MIN" + return None, None, None, None, False, None, None, None, "INPUT_VOLTAGE_BELOW_MIN" if voltage > max_input_voltage: print(RED + f"Input voltage is above the maximum allowed value of {max_input_voltage}mV! Stopping current benchmark." + RESET) - return None, None, None, False, None, "INPUT_VOLTAGE_ABOVE_MAX" + return None, None, None, None, False, None, None, None, "INPUT_VOLTAGE_ABOVE_MAX" hash_rate = info.get("hashRate") power_consumption = info.get("power") - + fan_speed = info.get("fanspeed") + if hash_rate is None or power_consumption is None: print(YELLOW + "Hashrate or Watts data not available." + RESET) - return None, None, None, False, None, "HASHRATE_POWER_DATA_FAILURE" + return None, None, None, None, False, None, None, None, "HASHRATE_POWER_DATA_FAILURE" if power_consumption > max_power: print(RED + f"Power consumption exceeded {max_power}W! Stopping current benchmark." + RESET) - return None, None, None, False, None, "POWER_CONSUMPTION_EXCEEDED" + return None, None, None, None, False, None, None, None, "POWER_CONSUMPTION_EXCEEDED" hash_rates.append(hash_rate) + s1 += hash_rate + s2 += hash_rate * hash_rate temperatures.append(temp) power_consumptions.append(power_consumption) if vr_temp is not None and vr_temp > 0: vr_temps.append(vr_temp) + if fan_speed is not None: + fan_speeds.append(fan_speed) # Calculate percentage progress percentage_progress = ((sample + 1) / total_samples) * 100 + running_sd = running_stddev(sample + 1, s1, s2) status_line = ( f"[{sample + 1:2d}/{total_samples:2d}] " f"{percentage_progress:5.1f}% | " f"CV: {core_voltage:4d}mV | " f"F: {frequency:4d}MHz | " f"H: {int(hash_rate):4d} GH/s | " + f"SD: {running_sd:3.0f} GH/s | " f"IV: {int(voltage):4d}mV | " f"T: {int(temp):2d}°C" ) if vr_temp is not None and vr_temp > 0: status_line += f" | VR: {int(vr_temp):2d}°C" + + # Add Power (Watts) to the status line if available + if power_consumption is not None: + status_line += f" | P: {int(power_consumption):2d} W" + + # Add Fan Speed to the status line if available + if fan_speed is not None: + status_line += f" | FAN: {int(fan_speed):2d}%" + print(status_line + RESET) # Only sleep if it's not the last iteration @@ -315,6 +475,7 @@ def benchmark_iteration(core_voltage, frequency): sorted_hashrates = sorted(hash_rates) trimmed_hashrates = sorted_hashrates[3:-3] # Remove first 3 and last 3 elements average_hashrate = sum(trimmed_hashrates) / len(trimmed_hashrates) + hashrate_stdev = statistics.stdev(trimmed_hashrates) if len(trimmed_hashrates) > 1 else 0.0 # Sort and trim temperatures (remove lowest 6 readings during warmup) sorted_temps = sorted(temperatures) @@ -329,33 +490,38 @@ def benchmark_iteration(core_voltage, frequency): average_vr_temp = sum(trimmed_vr_temps) / len(trimmed_vr_temps) average_power = sum(power_consumptions) / len(power_consumptions) + + average_fan_speed = None + if fan_speeds: + average_fan_speed = sum(fan_speeds) / len(fan_speeds) + print(GREEN + f"Average Fan Speed: {average_fan_speed:.2f}%" + RESET) # Add protection against zero hashrate if average_hashrate > 0: efficiency_jth = average_power / (average_hashrate / 1_000) else: print(RED + "Warning: Zero hashrate detected, skipping efficiency calculation" + RESET) - return None, None, None, False, None, "ZERO_HASHRATE" + return None, None, None, None, False, None, None, None, "ZERO_HASHRATE" # Calculate if hashrate is within 6% of expected hashrate_within_tolerance = (average_hashrate >= expected_hashrate * 0.94) print(GREEN + f"Average Hashrate: {average_hashrate:.2f} GH/s (Expected: {expected_hashrate:.2f} GH/s)" + RESET) + print(GREEN + f"Hashrate Std Dev: {hashrate_stdev:.2f} GH/s" + RESET) print(GREEN + f"Average Temperature: {average_temperature:.2f}°C" + RESET) if average_vr_temp is not None: print(GREEN + f"Average VR Temperature: {average_vr_temp:.2f}°C" + RESET) print(GREEN + f"Efficiency: {efficiency_jth:.2f} J/TH" + RESET) - return average_hashrate, average_temperature, efficiency_jth, hashrate_within_tolerance, average_vr_temp, None + return average_hashrate, hashrate_stdev, average_temperature, efficiency_jth, hashrate_within_tolerance, average_vr_temp, average_power, average_fan_speed, None else: print(YELLOW + "No Hashrate or Temperature or Watts data collected." + RESET) - return None, None, None, False, None, "NO_DATA_COLLECTED" + return None, None, None, None, False, None, None, None, "NO_DATA_COLLECTED" def save_results(): try: - # Extract IP from bitaxe_ip global variable and remove 'http://' - ip_address = bitaxe_ip.replace('http://', '') - filename = f"bitaxe_benchmark_results_{ip_address}_{START_TIME}.json" + # Refactored filename computation + filename = result_filename() with open(filename, "w") as f: json.dump(results, f, indent=4) print(GREEN + f"Results saved to {filename}" + RESET) @@ -369,16 +535,43 @@ def reset_to_best_setting(): print(YELLOW + "No valid benchmarking results found. Applying predefined default settings." + RESET) set_system_settings(default_voltage, default_frequency) else: + # Find best hashrate result best_result = sorted(results, key=lambda x: x["averageHashRate"], reverse=True)[0] best_voltage = best_result["coreVoltage"] best_frequency = best_result["frequency"] - print(GREEN + f"Applying the best settings from benchmarking:\n" - f" Core Voltage: {best_voltage}mV\n" - f" Frequency: {best_frequency}MHz" + RESET) + # Find most efficient result + efficient_result = sorted(results, key=lambda x: x["efficiencyJTH"], reverse=False)[0] + eff_voltage = efficient_result["coreVoltage"] + eff_frequency = efficient_result["frequency"] + + print(GREEN + "\n--- Benchmark Complete ---" + RESET) + print(GREEN + f"Best Hashrate Settings: {best_voltage}mV / {best_frequency}MHz ({best_result['averageHashRate']:.2f} GH/s)" + RESET) + print(GREEN + f"Most Efficient Settings: {eff_voltage}mV / {eff_frequency}MHz ({efficient_result['efficiencyJTH']:.2f} J/TH)" + RESET) + + print(YELLOW + "\nWARNING: 'Best Hashrate' settings are often at the thermal/stability limit." + RESET) + print(YELLOW + "Running these settings 24/7 may reduce hardware lifespan." + RESET) + print(YELLOW + "Applying 'Most Efficient' settings is generally safer for long-term use." + RESET) + + # For now, we still default to applying the "Best Hashrate" settings as per original behavior, + # but we've added the warning. + print(GREEN + "\nApplying Best Hashrate settings..." + RESET) set_system_settings(best_voltage, best_frequency) - restart_system() + # restart_system() is already called inside set_system_settings, so we don't need to call it again here. + +# --- Main execution logic --- +if args.set_values: + print(GREEN + "\n--- Applying Settings Only ---" + RESET) + print(GREEN + f"Applying Core Voltage: {initial_voltage}mV, Frequency: {initial_frequency}MHz to Bitaxe." + RESET) + + # Call the existing set_system_settings function + if set_system_settings(initial_voltage, initial_frequency): + print(GREEN + "Settings applied. Check your Bitaxe web interface to confirm." + RESET) + sys.exit(0) + else: + print(RED + "Failed to apply settings or stabilization failed. Check your Bitaxe." + RESET) + sys.exit(1) # Main benchmarking process try: @@ -394,23 +587,43 @@ def reset_to_best_setting(): current_voltage = initial_voltage current_frequency = initial_frequency + retry_upon_overheat = 0 while current_voltage <= max_allowed_voltage and current_frequency <= max_allowed_frequency: - set_system_settings(current_voltage, current_frequency) - avg_hashrate, avg_temp, efficiency_jth, hashrate_ok, avg_vr_temp, error_reason = benchmark_iteration(current_voltage, current_frequency) + if not set_system_settings(current_voltage, current_frequency): + # If stabilization failed (e.g. overheat during boot), treat as a failed iteration + avg_hashrate = None + hashrate_stdev = None + avg_temp = None + efficiency_jth = None + hashrate_ok = False + avg_vr_temp = None + avg_power = None + avg_fan_speed = None + error_reason = "STABILIZATION_FAILED" + else: + avg_hashrate, hashrate_stdev, avg_temp, efficiency_jth, hashrate_ok, avg_vr_temp, avg_power, avg_fan_speed, error_reason = benchmark_iteration(current_voltage, current_frequency) if avg_hashrate is not None and avg_temp is not None and efficiency_jth is not None: + retry_upon_overheat = 0 result = { "coreVoltage": current_voltage, "frequency": current_frequency, "averageHashRate": avg_hashrate, + "hashrateStdDev": hashrate_stdev, "averageTemperature": avg_temp, - "efficiencyJTH": efficiency_jth + "efficiencyJTH": efficiency_jth, + "averagePower": avg_power, + "errorReason": error_reason } # Only add VR temp if it exists if avg_vr_temp is not None: result["averageVRTemp"] = avg_vr_temp + + # Only add Fan Speed if it exists (assuming it's not None) + if avg_fan_speed is not None: + result["averageFanSpeed"] = avg_fan_speed results.append(result) @@ -418,20 +631,54 @@ def reset_to_best_setting(): # If hashrate is good, try increasing frequency if current_frequency + frequency_increment <= max_allowed_frequency: current_frequency += frequency_increment + print(GREEN + "Hashrate is good. Increasing frequency for next try." + RESET) else: + print(GREEN + "Reached max frequency with good results. Stopping further testing." + RESET) break # We've reached max frequency with good results else: # If hashrate is not good, go back one frequency step and increase voltage if current_voltage + voltage_increment <= max_allowed_voltage: current_voltage += voltage_increment - current_frequency -= frequency_increment # Go back to one frequency step and retry - print(YELLOW + f"Hashrate to low compared to expected. Decreasing frequency to {current_frequency}MHz and increasing voltage to {current_voltage}mV" + RESET) + + # Decrease frequency but respect the minimum limit + if current_frequency - frequency_increment >= min_allowed_frequency: + current_frequency -= frequency_increment + else: + current_frequency = min_allowed_frequency + + print(YELLOW + f"Hashrate too low compared to expected. Adjusting to {current_frequency}MHz and increasing voltage to {current_voltage}mV" + RESET) else: + print(YELLOW + "Reached max voltage without good results. Stopping further testing." + RESET) break # We've reached max voltage without good results else: # If we hit thermal limits or other issues, we've found the highest safe settings - print(GREEN + "Reached thermal or stability limits. Stopping further testing." + RESET) - break # Stop testing higher values + # In case of max Chip Temperature reached, continue loop to next voltage with decreased frequency + # Condition added to avoid successive overheat tries and reset to high initial frequency + + # CRITICAL SAFETY CHECK: If we overheated at the INITIAL frequency, do NOT increase voltage. + # Increasing voltage will only make it hotter. We should stop or decrease frequency. + if error_reason == "CHIP_TEMP_EXCEEDED" and current_frequency == initial_frequency: + print(RED + "Overheated at initial frequency! Cannot increase voltage safely. Stopping." + RESET) + break + + overheat_retry_allowed = ( + error_reason == "CHIP_TEMP_EXCEEDED" + and retry_upon_overheat < 1 + and initial_frequency <= current_frequency + frequency_increment + ) + if overheat_retry_allowed: + # If overheat, return to initial frequency while increasing voltage (considering max_allowed_voltage) + retry_upon_overheat += 1 + if current_voltage + voltage_increment <= max_allowed_voltage: + current_frequency = initial_frequency + current_voltage += voltage_increment + print(GREEN + "Reached thermal limit for the current voltage/frequency. Switching to next voltage increment." + RESET) + else: + print(GREEN + "Reached thermal limit for the current voltage/frequency. Next voltage increment out of voltage limit. Stopping further testing." + RESET) + break # We've reached max voltage, can't increase voltage anymore + else: + print(GREEN + "Reached thermal or stability limits. Stopping further testing." + RESET) + break # Stop testing higher values save_results() @@ -443,7 +690,6 @@ def reset_to_best_setting(): else: print(YELLOW + "No valid benchmarking results found. Applying predefined default settings." + RESET) set_system_settings(default_voltage, default_frequency) - restart_system() finally: if not system_reset_done: if results: @@ -453,7 +699,6 @@ def reset_to_best_setting(): else: print(YELLOW + "No valid benchmarking results found. Applying predefined default settings." + RESET) set_system_settings(default_voltage, default_frequency) - restart_system() system_reset_done = True # Print results summary only if we have results @@ -471,9 +716,12 @@ def reset_to_best_setting(): "coreVoltage": result["coreVoltage"], "frequency": result["frequency"], "averageHashRate": result["averageHashRate"], + "hashrateStdDev": result["hashrateStdDev"], "averageTemperature": result["averageTemperature"], "efficiencyJTH": result["efficiencyJTH"], - **({"averageVRTemp": result["averageVRTemp"]} if "averageVRTemp" in result else {}) + "averagePower": result["averagePower"], + **({"averageVRTemp": result["averageVRTemp"]} if "averageVRTemp" in result else {}), + **({"averageFanSpeed": result["averageFanSpeed"]} if "averageFanSpeed" in result else {}) } for i, result in enumerate(top_5_results, 1) ], @@ -483,17 +731,20 @@ def reset_to_best_setting(): "coreVoltage": result["coreVoltage"], "frequency": result["frequency"], "averageHashRate": result["averageHashRate"], + "hashrateStdDev": result["hashrateStdDev"], "averageTemperature": result["averageTemperature"], "efficiencyJTH": result["efficiencyJTH"], - **({"averageVRTemp": result["averageVRTemp"]} if "averageVRTemp" in result else {}) + "averagePower": result["averagePower"], + **({"averageVRTemp": result["averageVRTemp"]} if "averageVRTemp" in result else {}), + **({"averageFanSpeed": result["averageFanSpeed"]} if "averageFanSpeed" in result else {}) } for i, result in enumerate(top_5_efficient_results, 1) ] } # Save the final data to JSON - ip_address = bitaxe_ip.replace('http://', '') - filename = f"bitaxe_benchmark_results_{ip_address}_{START_TIME}.json" + # Refactored filename computation + filename = result_filename() with open(filename, "w") as f: json.dump(final_data, f, indent=4) @@ -505,8 +756,12 @@ def reset_to_best_setting(): print(GREEN + f" Core Voltage: {result['coreVoltage']}mV" + RESET) print(GREEN + f" Frequency: {result['frequency']}MHz" + RESET) print(GREEN + f" Average Hashrate: {result['averageHashRate']:.2f} GH/s" + RESET) + print(GREEN + f" Hashrate Std Dev: {result.get('hashrateStdDev', 0.0):.2f} GH/s" + RESET) print(GREEN + f" Average Temperature: {result['averageTemperature']:.2f}°C" + RESET) print(GREEN + f" Efficiency: {result['efficiencyJTH']:.2f} J/TH" + RESET) + print(GREEN + f" Average Power: {result['averagePower']:.2f} W" + RESET) + if "averageFanSpeed" in result: + print(GREEN + f" Average Fan Speed: {result['averageFanSpeed']:.2f}%" + RESET) if "averageVRTemp" in result: print(GREEN + f" Average VR Temperature: {result['averageVRTemp']:.2f}°C" + RESET) @@ -516,30 +771,15 @@ def reset_to_best_setting(): print(GREEN + f" Core Voltage: {result['coreVoltage']}mV" + RESET) print(GREEN + f" Frequency: {result['frequency']}MHz" + RESET) print(GREEN + f" Average Hashrate: {result['averageHashRate']:.2f} GH/s" + RESET) + print(GREEN + f" Hashrate Std Dev: {result.get('hashrateStdDev', 0.0):.2f} GH/s" + RESET) print(GREEN + f" Average Temperature: {result['averageTemperature']:.2f}°C" + RESET) print(GREEN + f" Efficiency: {result['efficiencyJTH']:.2f} J/TH" + RESET) + print(GREEN + f" Average Power: {result['averagePower']:.2f} W" + RESET) + if "averageFanSpeed" in result: + print(GREEN + f" Average Fan Speed: {result['averageFanSpeed']:.2f}%" + RESET) if "averageVRTemp" in result: print(GREEN + f" Average VR Temperature: {result['averageVRTemp']:.2f}°C" + RESET) else: print(RED + "No valid results were found during benchmarking." + RESET) -# Add this new function to handle cleanup -def cleanup_and_exit(reason=None): - global system_reset_done - if system_reset_done: - return - - try: - if results: - reset_to_best_setting() - save_results() - print(GREEN + "Bitaxe reset to best settings and results saved." + RESET) - else: - print(YELLOW + "No valid benchmarking results found. Applying predefined default settings." + RESET) - set_system_settings(default_voltage, default_frequency) - finally: - system_reset_done = True - if reason: - print(RED + f"Benchmarking stopped: {reason}" + RESET) - print(GREEN + "Benchmarking completed." + RESET) - sys.exit(0) +