Compare commits

...

14 Commits

Author SHA1 Message Date
UpstreamData
595467487b fixed a bug with the way the hashrate total works when getting new data on a small subset of miners 2022-01-05 09:42:37 -07:00
UpstreamData
8cba08a900 removed "Identifying..." from scanning 2022-01-05 09:31:09 -07:00
UpstreamData
89c009ab11 improved the functionality of the scan 2022-01-05 09:27:31 -07:00
UpstreamData
38f93fa212 improved the functionality of get data greatly 2022-01-05 09:17:45 -07:00
UpstreamData
eac2d64468 changed the 2 listboxes with IPs and data into a table, and fixed all functions using this 2022-01-05 08:59:38 -07:00
UpstreamData
8a2cef15b2 fixed a bug with the build because importing from passlib is buggy 2022-01-04 10:23:08 -07:00
UpstreamData
c075f3f66a added more doocstrings and improved the readme 2022-01-04 09:16:17 -07:00
UpstreamData
d138778f0a added BTMiner docstrings 2022-01-04 09:01:38 -07:00
UpstreamData
cf3aefc201 updated btminer API to use cryptography instead of pycryptodome because it's painful to set up, and updated requirements.txt 2022-01-03 16:18:57 -07:00
UpstreamData
d974be5329 finished bosminer docs 2022-01-03 15:17:09 -07:00
UpstreamData
8c147283ba fixed a bug with sending commands which led to a pattern of recursive commands blocking the program forever 2022-01-03 15:13:33 -07:00
UpstreamData
f72ba6582d added docstrings for CGMiner API, and improved BMMiner docstrings 2022-01-03 13:44:15 -07:00
UpstreamData
b65badf097 improved the build process and added a readme that gets added into the cfg util and its builds 2022-01-03 13:17:32 -07:00
UpstreamData
cea71d8ca1 added a new window to generate configs 2022-01-03 13:16:38 -07:00
15 changed files with 1005 additions and 153 deletions

View File

@@ -53,7 +53,22 @@ class BaseMinerAPI:
# standard multicommand format is "command1+command2" # standard multicommand format is "command1+command2"
# doesnt work for S19 which is dealt with in the send command function # doesnt work for S19 which is dealt with in the send command function
command = "+".join(commands) command = "+".join(commands)
return await self.send_command(command) data = None
try:
data = await self.send_command(command)
except APIError:
try:
data = {}
# S19 handler, try again
for cmd in command.split("+"):
data[cmd] = []
data[cmd].append(await self.send_command(cmd))
except APIError as e:
raise APIError(e)
except Exception as e:
print(e)
if data:
return data
async def send_command(self, command: str, parameters: str or int or bool = None) -> dict: async def send_command(self, command: str, parameters: str or int or bool = None) -> dict:
"""Send an API command to the miner and return the result.""" """Send an API command to the miner and return the result."""
@@ -95,18 +110,6 @@ class BaseMinerAPI:
await writer.wait_closed() await writer.wait_closed()
# validate the command suceeded # validate the command suceeded
# also handle for S19 not liking "command1+command2" format
if not self.validate_command_output(data):
try:
data = {}
# S19 handler, try again
for cmd in command.split("+"):
data[cmd] = []
data[cmd].append(await self.send_command(cmd))
except Exception as e:
print(e)
# check again after second try
if not self.validate_command_output(data): if not self.validate_command_output(data):
raise APIError(data["STATUS"][0]["Msg"]) raise APIError(data["STATUS"][0]["Msg"])

View File

@@ -29,7 +29,7 @@ class BMMinerAPI(BaseMinerAPI):
""" """
API 'config' command. API 'config' command.
Returns some miner configuration information: Returns a dict containing some miner configuration information:
ASC Count <- the number of ASCs ASC Count <- the number of ASCs
PGA Count <- the number of PGAs PGA Count <- the number of PGAs
Pool Count <- the number of Pools Pool Count <- the number of Pools
@@ -446,7 +446,6 @@ class BMMinerAPI(BaseMinerAPI):
Parameters: Parameters:
n: the number of the ASC to disable. n: the number of the ASC to disable.
""" """
return await self.send_command("ascdisable", parameters=n) return await self.send_command("ascdisable", parameters=n)

View File

@@ -2,49 +2,146 @@ from API import BaseMinerAPI
class BOSMinerAPI(BaseMinerAPI): class BOSMinerAPI(BaseMinerAPI):
"""
A class that abstracts the BOSMiner API in the miners.
Each method corresponds to an API command in BOSMiner.
BOSMiner API documentation:
https://docs.braiins.com/os/plus-en/Development/1_api.html
Parameters:
ip: the IP address of the miner.
port (optional): the port of the API on the miner (standard is 4028)
"""
def __init__(self, ip, port=4028): def __init__(self, ip, port=4028):
super().__init__(ip, port) super().__init__(ip, port)
async def asccount(self) -> dict: async def asccount(self) -> dict:
"""
API 'asccount' command.
Returns a dict containing the number of ASC devices.
"""
return await self.send_command("asccount") return await self.send_command("asccount")
async def asc(self, n: int) -> dict: async def asc(self, n: int) -> dict:
"""
API 'asc' command.
Returns a dict containing the details of a single ASC of number N.
n: the ASC device to get details of.
"""
return await self.send_command("asc", parameters=n) return await self.send_command("asc", parameters=n)
async def devdetails(self) -> dict: async def devdetails(self) -> dict:
"""
API 'devdetails' command.
Returns a dict containing all devices with their static details.
"""
return await self.send_command("devdetails") return await self.send_command("devdetails")
async def devs(self) -> dict: async def devs(self) -> dict:
"""
API 'devs' command.
Returns a dict containing each PGA/ASC with their details.
"""
return await self.send_command("devs") return await self.send_command("devs")
async def edevs(self, old: bool = False) -> dict: async def edevs(self, old: bool = False) -> dict:
"""
API 'edevs' command.
Returns a dict containing each PGA/ASC with their details,
ignoring blacklisted devices and zombie devices.
Parameters:
old (optional): include zombie devices that became zombies less than 'old' seconds ago
"""
if old: if old:
return await self.send_command("edevs", parameters="old") return await self.send_command("edevs", parameters="old")
else: else:
return await self.send_command("edevs") return await self.send_command("edevs")
async def pools(self) -> dict: async def pools(self) -> dict:
"""
API 'pools' command.
Returns a dict containing the status of each pool.
"""
return await self.send_command("pools") return await self.send_command("pools")
async def summary(self) -> dict: async def summary(self) -> dict:
"""
API 'summary' command.
Returns a dict containing the status summary of the miner.
"""
return await self.send_command("summary") return await self.send_command("summary")
async def stats(self) -> dict: async def stats(self) -> dict:
"""
API 'stats' command.
Returns a dict containing stats for all device/pool with more than 1 getwork.
"""
return await self.send_command("stats") return await self.send_command("stats")
async def version(self) -> dict: async def version(self) -> dict:
"""
API 'version' command.
Returns a dict containing version information.
"""
return await self.send_command("version") return await self.send_command("version")
async def estats(self) -> dict: async def estats(self) -> dict:
"""
API 'estats' command.
Returns a dict containing stats for all device/pool with more than 1 getwork,
ignoring zombie devices.
Parameters:
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
"""
return await self.send_command("estats") return await self.send_command("estats")
async def check(self) -> dict: async def check(self, command: str) -> dict:
return await self.send_command("check") """
API 'check' command.
Returns information about a command:
Exists (Y/N) <- the command exists in this version
Access (Y/N) <- you have access to use the command
Parameters:
command: the command to get information about.
"""
return await self.send_command("check", parameters=command)
async def coin(self) -> dict: async def coin(self) -> dict:
"""
API 'coin' command.
Returns information about the current coin being mined:
Hash Method <- the hashing algorithm
Current Block Time <- blocktime as a float, 0 means none
Current Block Hash <- the hash of the current block, blank means none
LP <- whether LP is in use on at least 1 pool
Network Difficulty: the current network difficulty
"""
return await self.send_command("coin") return await self.send_command("coin")
async def lcd(self) -> dict: async def lcd(self) -> dict:
"""
API 'lcd' command.
Returns a dict containing an all in one status summary of the miner.
"""
return await self.send_command("lcd") return await self.send_command("lcd")
async def switchpool(self, n: int) -> dict: async def switchpool(self, n: int) -> dict:
@@ -73,19 +170,53 @@ class BOSMinerAPI(BaseMinerAPI):
# return await self.send_command("removepool", parameters=n) # return await self.send_command("removepool", parameters=n)
async def fans(self) -> dict: async def fans(self) -> dict:
"""
API 'fans' command.
Returns a dict containing information on fans and fan speeds.
"""
return await self.send_command("fans") return await self.send_command("fans")
async def tempctrl(self) -> dict: async def tempctrl(self) -> dict:
"""
API 'tempctrl' command.
Returns a dict containing temp control configuration.
"""
return await self.send_command("tempctrl") return await self.send_command("tempctrl")
async def temps(self) -> dict: async def temps(self) -> dict:
"""
API 'temps' command.
Returns a dict containing temperature information.
"""
return await self.send_command("temps") return await self.send_command("temps")
async def tunerstatus(self) -> dict: async def tunerstatus(self) -> dict:
"""
API 'tunerstatus' command.
Returns a dict containing tuning stats.
"""
return await self.send_command("tunerstatus") return await self.send_command("tunerstatus")
async def pause(self) -> dict: async def pause(self) -> dict:
"""
API 'pause' command.
Pauses mining and stops power consumption and waits for resume command.
Returns a dict stating that the miner paused mining.
"""
return await self.send_command("pause") return await self.send_command("pause")
async def resume(self) -> dict: async def resume(self) -> dict:
"""
API 'pause' command.
Resumes mining on the miner.
Returns a dict stating that the miner resumed mining.
"""
return await self.send_command("resume") return await self.send_command("resume")

View File

@@ -1,12 +1,12 @@
from API import BaseMinerAPI, APIError from API import BaseMinerAPI, APIError
from passlib.hash import md5_crypt from passlib.handlers import md5_crypt
import asyncio import asyncio
import re import re
import json import json
import hashlib import hashlib
import binascii import binascii
from Crypto.Cipher import AES from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import base64 import base64
@@ -42,13 +42,19 @@ def _add_to_16(s: str) -> bytes:
def parse_btminer_priviledge_data(token_data, data): def parse_btminer_priviledge_data(token_data, data):
# get the encoded data from the dict
enc_data = data['enc'] enc_data = data['enc']
# get the aes key from the token data
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest() aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
# unhexlify the aes key
aeskey = binascii.unhexlify(aeskey.encode()) aeskey = binascii.unhexlify(aeskey.encode())
aes = AES.new(aeskey, AES.MODE_ECB) # create the required decryptor
ret_msg = json.loads(str( aes = Cipher(algorithms.AES(aeskey), modes.ECB())
aes.decrypt(base64.decodebytes(bytes( decryptor = aes.decryptor()
enc_data, encoding='utf8'))).rstrip(b'\0').decode("utf8"))) # decode the message with the decryptor
ret_msg = json.loads(decryptor.update(
base64.decodebytes(bytes(enc_data, encoding='utf8'))
).rstrip(b'\0').decode("utf8"))
return ret_msg return ret_msg
@@ -60,13 +66,12 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
# unhexlify the encoded host_passwd # unhexlify the encoded host_passwd
aeskey = binascii.unhexlify(aeskey.encode()) aeskey = binascii.unhexlify(aeskey.encode())
# create a new AES key # create a new AES key
aes = AES.new(aeskey, AES.MODE_ECB) aes = Cipher(algorithms.AES(aeskey), modes.ECB())
encryptor = aes.encryptor()
# dump the command to json # dump the command to json
api_json_str = json.dumps(command) api_json_str = json.dumps(command)
# encode the json command with the aes key # encode the json command with the aes key
api_json_str_enc = str(base64.encodebytes( api_json_str_enc = base64.encodebytes(encryptor.update(_add_to_16(api_json_str))).decode("utf-8").replace("\n", "")
aes.encrypt(_add_to_16(api_json_str))),
encoding='utf8').replace('\n', '')
# label the data as being encoded # label the data as being encoded
data_enc = {'enc': 1, 'data': api_json_str_enc} data_enc = {'enc': 1, 'data': api_json_str_enc}
# dump the labeled data to json # dump the labeled data to json
@@ -82,7 +87,9 @@ class BTMinerAPI(BaseMinerAPI):
async def send_command(self, command: str | bytes, **kwargs) -> dict: async def send_command(self, command: str | bytes, **kwargs) -> dict:
"""Send an API command to the miner and return the result.""" """Send an API command to the miner and return the result."""
# check if command is a string, if its bytes its encoded and needs to be send raw
if isinstance(command, str): if isinstance(command, str):
# if it is a string, put it into the standard command format
command = json.dumps({"command": command}).encode("utf-8") command = json.dumps({"command": command}).encode("utf-8")
try: try:
# get reader and writer streams # get reader and writer streams
@@ -116,18 +123,27 @@ class BTMinerAPI(BaseMinerAPI):
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
# check if th returned data is encoded
if 'enc' in data.keys(): if 'enc' in data.keys():
# try to parse the encoded data
try: try:
data = parse_btminer_priviledge_data(self.current_token, data) data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e: except Exception as e:
print(e) print(e)
# if it fails to validate, it is likely an error
if not self.validate_command_output(data): if not self.validate_command_output(data):
raise APIError(data["Msg"]) raise APIError(data["Msg"])
# return the parsed json as a dict
return data return data
async def get_token(self): async def get_token(self):
"""
API 'get_token' command.
Returns an encoded token and md5 password, which are used for the privileged API.
"""
data = await self.send_command("get_token") data = await self.send_command("get_token")
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$') pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$')
pwd = pwd.split('$') pwd = pwd.split('$')
@@ -138,7 +154,7 @@ class BTMinerAPI(BaseMinerAPI):
self.current_token = {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5} self.current_token = {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
return {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5} return {'host_sign': host_sign, 'host_passwd_md5': host_passwd_md5}
#### privileged COMMANDS #### #### PRIVILEGED COMMANDS ####
# Please read the top of this file to learn # Please read the top of this file to learn
# how to configure the whatsminer API to # how to configure the whatsminer API to
# use these commands. # use these commands.
@@ -147,8 +163,13 @@ class BTMinerAPI(BaseMinerAPI):
pool_1: str, worker_1: str, passwd_1: str, pool_1: str, worker_1: str, passwd_1: str,
pool_2: str = None, worker_2: str = None, passwd_2: str = None, pool_2: str = None, worker_2: str = None, passwd_2: str = None,
pool_3: str = None, worker_3: str = None, passwd_3: str = None): pool_3: str = None, worker_3: str = None, passwd_3: str = None):
# get the token and password from the miner
token_data = await self.get_token() token_data = await self.get_token()
if pool_2 and pool_3:
# parse pool data
if not pool_1:
raise APIError("No pools set.")
elif pool_2 and pool_3:
command = { command = {
"cmd": "update_pools", "cmd": "update_pools",
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1, "pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
@@ -166,16 +187,33 @@ class BTMinerAPI(BaseMinerAPI):
"cmd": "update_pools", "cmd": "update_pools",
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1, "pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
} }
# encode the command with the token data
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
# send the command
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def restart_btminer(self): async def restart(self):
"""
API 'restart_btminer' command
Returns a reply informing of the restart and restarts BTMiner.
"""
command = {"cmd": "restart_btminer"} command = {"cmd": "restart_btminer"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def power_off(self, respbefore: bool = True): async def power_off(self, respbefore: bool = True):
"""
API 'power_off' command.
Powers off the mining of the miner.
Returns info on the power off.
Parameters:
respbefore (optional): respond before powering off.
"""
if respbefore: if respbefore:
command = {"cmd": "power_off", "respbefore": "true"} command = {"cmd": "power_off", "respbefore": "true"}
else: else:
@@ -185,24 +223,58 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def power_on(self): async def power_on(self):
"""
API 'power_on' command.
Powers on the mining of the miner.
Returns info on the power on.
"""
command = {"cmd": "power_on"} command = {"cmd": "power_on"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def reset_led(self): async def reset_led(self):
"""
API 'reset_led' command.
Resets the LED flashing to normal.
Returns a confirmation of resetting the LED.
"""
command = {"cmd": "set_led", "param": "auto"} command = {"cmd": "set_led", "param": "auto"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def set_led(self, color: str = "red", period: int = 2000, duration: int = 1000, start: int = 0): async def set_led(self, color: str = "red", period: int = 2000, duration: int = 1000, start: int = 0):
"""
API 'set_led' command.
Sets the LED to do some pattern set with parameters.
Returns a confirmation of setting the LED.
Parameters:
color: 'red' or 'green'
period: flash cycle in ms
duration: led on time in the cycle in ms
start: led on time offset in the cycle in ms
"""
command = {"cmd": "set_led", "color": color, "period": period, "duration": duration, "start": start} command = {"cmd": "set_led", "color": color, "period": period, "duration": duration, "start": start}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def set_low_power(self): async def set_low_power(self):
"""
API 'set_low_power' command.
Sets the miner to low power mode.
Returns the status of setting the miner to low power mode.
"""
command = {"cmd": "set_low_power"} command = {"cmd": "set_low_power"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
@@ -214,18 +286,43 @@ class BTMinerAPI(BaseMinerAPI):
return NotImplementedError return NotImplementedError
async def reboot(self): async def reboot(self):
"""
API 'reboot' command.
Reboots the miner.
Returns the status of the command then reboots.
"""
command = {"cmd": "reboot"} command = {"cmd": "reboot"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def factory_reset(self): async def factory_reset(self):
"""
API 'factory_reset' command.
Resets the miner to factory defaults.
Returns the status of the command then resets.
"""
command = {"cmd": "factory_reset"} command = {"cmd": "factory_reset"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def update_pwd(self, old_pwd: str, new_pwd: str): async def update_pwd(self, old_pwd: str, new_pwd: str):
"""
API 'update_pwd' command.
Updates the admin user's password.
Returns the status of setting the password to the new password.
Parameters:
old_pwd: the old admin password.
new_pwd: the new password to set. Max length of 8 bytes, using letters, numbers, and underscores.
"""
# check if password length is greater than 8 bytes # check if password length is greater than 8 bytes
if len(new_pwd.encode('utf-8')) > 8: if len(new_pwd.encode('utf-8')) > 8:
return APIError( return APIError(
@@ -236,6 +333,13 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def set_target_freq(self, percent: int): async def set_target_freq(self, percent: int):
"""
API 'set_target_freq' command.
Sets the frequency for the miner ot use.
Returns the status of setting the frequency.
"""
if not -10 < percent < 100: if not -10 < percent < 100:
return APIError(f"Frequency % is outside of the allowed range. Please set a % between -10 and 100") return APIError(f"Frequency % is outside of the allowed range. Please set a % between -10 and 100")
command = {"cmd": "set_target_freq", "percent": str(percent)} command = {"cmd": "set_target_freq", "percent": str(percent)}
@@ -244,36 +348,82 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def enable_fast_boot(self): async def enable_fast_boot(self):
"""
API 'enable_fast_boot' command.
Turns on the fast boot feature on the miner.
Returns the status of setting the fast boot to on.
"""
command = {"cmd": "enable_btminer_fast_boot"} command = {"cmd": "enable_btminer_fast_boot"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def disable_fast_boot(self): async def disable_fast_boot(self):
"""
API 'disable'_fast_boot' command.
Turns off the fast boot feature on the miner.
Returns the status of setting the fast boot to off.
"""
command = {"cmd": "disable_btminer_fast_boot"} command = {"cmd": "disable_btminer_fast_boot"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def enable_web_pools(self): async def enable_web_pools(self):
"""
API 'enable_web_pools' command.
Turns on the ability to change the pools through the web interface.
Returns the status of setting the web pools to enabled.
"""
command = {"cmd": "enable_web_pools"} command = {"cmd": "enable_web_pools"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def disable_web_pools(self): async def disable_web_pools(self):
"""
API 'disable_web_pools' command.
Turns off the ability to change the pools through the web interface.
Returns the status of setting the web pools to disabled.
"""
command = {"cmd": "disable_web_pools"} command = {"cmd": "disable_web_pools"}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def set_hostname(self, hostname: str): async def set_hostname(self, hostname: str):
"""
API 'set_hostname' command.
Sets the hostname of the miner.
Returns the status of setting the hostname.
"""
command = {"cmd": "set_hostname", "hostname": hostname} command = {"cmd": "set_hostname", "hostname": hostname}
token_data = await self.get_token() token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command) enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def set_power_pct(self, percent: int): async def set_power_pct(self, percent: int):
"""
API 'set_power_pct' command.
Sets the percent of power the miner should use.
Returns the status of setting the power usage to this percent.
Parameters:
percent: the percent to set the power usage to, between 0 and 100.
"""
if not 0 < percent < 100: if not 0 < percent < 100:
return APIError(f"Power PCT % is outside of the allowed range. Please set a % between 0 and 100") return APIError(f"Power PCT % is outside of the allowed range. Please set a % between 0 and 100")
command = {"cmd": "set_power_pct", "percent": str(percent)} command = {"cmd": "set_power_pct", "percent": str(percent)}
@@ -282,6 +432,18 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command) return await self.send_command(enc_command)
async def pre_power_on(self, complete: bool, msg: str): async def pre_power_on(self, complete: bool, msg: str):
"""
API 'pre_power_on' command.
Preheats the miner for the 'power_on' command. Can also be used to query the status of pre powering on.
Returns status of pre powering on.
Parameters:
complete: check whether or not it is complete.
msg: the message to check. "wait for adjust temp" or "adjust complete" or "adjust continue"
"""
if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue": if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue":
return APIError( return APIError(
'Message is incorrect, please choose one of ' 'Message is incorrect, please choose one of '
@@ -299,28 +461,88 @@ class BTMinerAPI(BaseMinerAPI):
#### END privileged COMMANDS #### #### END privileged COMMANDS ####
async def summary(self): async def summary(self):
"""
API 'summary' command.
Returns a dict containing the status summary of the miner.
"""
return await self.send_command("summary") return await self.send_command("summary")
async def pools(self): async def pools(self):
"""
API 'pools' command.
Returns a dict containing the status of each pool.
"""
return await self.send_command("pools") return await self.send_command("pools")
async def devs(self): async def devs(self):
"""
API 'devs' command.
Returns a dict containing each PGA/ASC with their details.
"""
return await self.send_command("devs") return await self.send_command("devs")
async def edevs(self): async def edevs(self):
"""
API 'edevs' command.
Returns a dict containing each PGA/ASC with their details,
ignoring blacklisted devices and zombie devices.
"""
return await self.send_command("edevs") return await self.send_command("edevs")
async def devdetails(self): async def devdetails(self):
"""
API 'devdetails' command.
Returns a dict containing all devices with their static details.
"""
return await self.send_command("devdetails") return await self.send_command("devdetails")
async def get_psu(self): async def get_psu(self):
"""
API 'get_psu' command.
Returns a dict containing PSU and power information.
"""
return await self.send_command("get_psu") return await self.send_command("get_psu")
async def version(self): async def version(self):
"""
API 'get_version' command.
Returns a dict containing version information.
"""
return await self.send_command("get_version") return await self.send_command("get_version")
async def status(self): async def status(self):
"""
API 'status' command.
Returns a dict containing BTMiner status and firmware version.
"""
return await self.send_command("status") return await self.send_command("status")
async def get_miner_info(self): async def get_miner_info(self, info: str | list):
return await self.send_command("get_miner_info") """
API 'get_miner_info' command.
Returns a dict containing requested information.
Parameters:
info: the info that you want to get.
"ip",
"proto",
"netmask",
"gateway",
"dns",
"hostname",
"mac",
"ledstat".
"""
if isinstance(info, str):
return await self.send_command("get_miner_info", parameters=info)
else:
return await self.send_command("get_miner_info", parameters=f"{','.join([str(item) for item in info])}")

View File

@@ -2,6 +2,18 @@ from API import BaseMinerAPI
class CGMinerAPI(BaseMinerAPI): class CGMinerAPI(BaseMinerAPI):
"""
A class that abstracts the CGMiner API in the miners.
Each method corresponds to an API command in CGMiner.
CGMiner API documentation:
https://github.com/ckolivas/cgminer/blob/master/API-README
Parameters:
ip: the IP address of the miner.
port (optional): the port of the API on the miner (standard is 4028)
"""
def __init__(self, ip, port=4028): def __init__(self, ip, port=4028):
super().__init__(ip, port) super().__init__(ip, port)
@@ -29,139 +41,492 @@ class CGMinerAPI(BaseMinerAPI):
return await self.send_command("config") return await self.send_command("config")
async def summary(self) -> dict: async def summary(self) -> dict:
"""
API 'summary' command.
Returns a dict containing the status summary of the miner.
"""
return await self.send_command("summary") return await self.send_command("summary")
async def pools(self) -> dict: async def pools(self) -> dict:
"""
API 'pools' command.
Returns a dict containing the status of each pool.
"""
return await self.send_command("pools") return await self.send_command("pools")
async def devs(self) -> dict: async def devs(self) -> dict:
"""
API 'devs' command.
Returns a dict containing each PGA/ASC with their details.
"""
return await self.send_command("devs") return await self.send_command("devs")
async def edevs(self, old: bool = False) -> dict: async def edevs(self, old: bool = False) -> dict:
"""
API 'edevs' command.
Returns a dict containing each PGA/ASC with their details,
ignoring blacklisted devices and zombie devices.
Parameters:
old (optional): include zombie devices that became zombies less than 'old' seconds ago
"""
if old: if old:
return await self.send_command("edevs", parameters="old") return await self.send_command("edevs", parameters="old")
else: else:
return await self.send_command("edevs") return await self.send_command("edevs")
async def pga(self, n: int) -> dict: async def pga(self, n: int) -> dict:
"""
API 'pga' command.
Returns a dict containing the details of a single PGA of number N.
Parameters:
n: the number of the PGA to get details of.
"""
return await self.send_command("pga", parameters=n) return await self.send_command("pga", parameters=n)
async def pgacount(self) -> dict: async def pgacount(self) -> dict:
"""
API 'pgacount' command.
Returns a dict containing the number of PGA devices.
"""
return await self.send_command("pgacount") return await self.send_command("pgacount")
async def switchpool(self, n: int) -> dict: async def switchpool(self, n: int) -> dict:
"""
API 'switchpool' command.
Returns the STATUS section with the results of switching pools.
Parameters:
n: the number of the pool to switch to.
"""
return await self.send_command("switchpool", parameters=n) return await self.send_command("switchpool", parameters=n)
async def enablepool(self, n: int) -> dict: async def enablepool(self, n: int) -> dict:
"""
API 'enablepool' command.
Returns the STATUS section with the results of enabling the pool.
Parameters:
n: the number of the pool to enable.
"""
return await self.send_command("enablepool", parameters=n) return await self.send_command("enablepool", parameters=n)
async def addpool(self, url: str, username: str, password: str) -> dict: async def addpool(self, url: str, username: str, password: str) -> dict:
"""
API 'addpool' command.
Returns the STATUS section with the results of adding the pool.
Parameters:
url: the URL of the new pool to add.
username: the users username on the new pool.
password: the worker password on the new pool.
"""
return await self.send_command("addpool", parameters=f"{url}, {username}, {password}") return await self.send_command("addpool", parameters=f"{url}, {username}, {password}")
async def poolpriority(self, *n: int) -> dict: async def poolpriority(self, *n: int) -> dict:
"""
API 'poolpriority' command.
Returns the STATUS section with the results of setting pool priority.
Parameters:
n: pool numbers in order of priority.
"""
return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}") return await self.send_command("poolpriority", parameters=f"{','.join([str(item) for item in n])}")
async def poolquota(self, n: int, q: int) -> dict: async def poolquota(self, n: int, q: int) -> dict:
"""
API 'poolquota' command.
Returns the STATUS section with the results of setting pool quota.
Parameters:
n: pool number to set quota on.
q: quota to set the pool to.
"""
return await self.send_command("poolquota", parameters=f"{n}, {q}") return await self.send_command("poolquota", parameters=f"{n}, {q}")
async def disablepool(self, n: int) -> dict: async def disablepool(self, n: int) -> dict:
"""
API 'disablepool' command.
Returns the STATUS section with the results of disabling the pool.
Parameters:
n: the number of the pool to disable.
"""
return await self.send_command("disablepool", parameters=n) return await self.send_command("disablepool", parameters=n)
async def removepool(self, n: int) -> dict: async def removepool(self, n: int) -> dict:
"""
API 'removepool' command.
Returns the STATUS section with the results of removing the pool.
Parameters:
n: the number of the pool to remove.
"""
return await self.send_command("removepool", parameters=n) return await self.send_command("removepool", parameters=n)
async def save(self, filename: str = None) -> dict: async def save(self, filename: str = None) -> dict:
"""
API 'save' command.
Returns the STATUS section with the results of saving the config file..
Parameters:
filename (optional): the filename to save the config as.
"""
if filename: if filename:
return await self.send_command("save", parameters=filename) return await self.send_command("save", parameters=filename)
else: else:
return await self.send_command("save") return await self.send_command("save")
async def quit(self) -> dict: async def quit(self) -> dict:
"""
API 'quit' command.
Returns a single "BYE" before CGMiner quits.
"""
return await self.send_command("quit") return await self.send_command("quit")
async def notify(self) -> dict: async def notify(self) -> dict:
"""
API 'notify' command.
Returns a dict containing the last status and count of each devices problem(s).
"""
return await self.send_command("notify") return await self.send_command("notify")
async def privileged(self) -> dict: async def privileged(self) -> dict:
"""
API 'privileged' command.
Returns the STATUS section with an error if you have no privileged access.
"""
return await self.send_command("privileged") return await self.send_command("privileged")
async def pgaenable(self, n: int) -> dict: async def pgaenable(self, n: int) -> dict:
"""
API 'pgaenable' command.
Returns the STATUS section with the results of enabling the PGA device N.
Parameters:
n: the number of the PGA to enable.
"""
return await self.send_command("pgaenable", parameters=n) return await self.send_command("pgaenable", parameters=n)
async def pgadisable(self, n: int) -> dict: async def pgadisable(self, n: int) -> dict:
"""
API 'pgadisable' command.
Returns the STATUS section with the results of disabling the PGA device N.
Parameters:
n: the number of the PGA to disable.
"""
return await self.send_command("pgadisable", parameters=n) return await self.send_command("pgadisable", parameters=n)
async def pgaidentify(self, n: int) -> dict: async def pgaidentify(self, n: int) -> dict:
"""
API 'pgaidentify' command.
Returns the STATUS section with the results of identifying the PGA device N.
Parameters:
n: the number of the PGA to identify.
"""
return await self.send_command("pgaidentify", parameters=n) return await self.send_command("pgaidentify", parameters=n)
async def devdetails(self) -> dict: async def devdetails(self) -> dict:
"""
API 'devdetails' command.
Returns a dict containing all devices with their static details.
"""
return await self.send_command("devdetails") return await self.send_command("devdetails")
async def restart(self) -> dict: async def restart(self) -> dict:
"""
API 'restart' command.
Returns a single "RESTART" before CGMiner restarts.
"""
return await self.send_command("restart") return await self.send_command("restart")
async def stats(self) -> dict: async def stats(self) -> dict:
"""
API 'stats' command.
Returns a dict containing stats for all device/pool with more than 1 getwork.
"""
return await self.send_command("stats") return await self.send_command("stats")
async def estats(self, old: bool = False) -> dict: async def estats(self, old: bool = False) -> dict:
"""
API 'estats' command.
Returns a dict containing stats for all device/pool with more than 1 getwork,
ignoring zombie devices.
Parameters:
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
"""
if old: if old:
return await self.send_command("estats", parameters="old") return await self.send_command("estats", parameters="old")
else: else:
return await self.send_command("estats") return await self.send_command("estats")
async def check(self, command) -> dict: async def check(self, command) -> dict:
"""
API 'check' command.
Returns information about a command:
Exists (Y/N) <- the command exists in this version
Access (Y/N) <- you have access to use the command
Parameters:
command: the command to get information about.
"""
return await self.send_command("check", parameters=command) return await self.send_command("check", parameters=command)
async def failover_only(self, failover: bool) -> dict: async def failover_only(self, failover: bool) -> dict:
"""
API 'failover-only' command.
Returns the STATUS section with what failover-only was set to.
Parameters:
failover: what to set failover-only to.
"""
return await self.send_command("failover-only", parameters=failover) return await self.send_command("failover-only", parameters=failover)
async def coin(self) -> dict: async def coin(self) -> dict:
"""
API 'coin' command.
Returns information about the current coin being mined:
Hash Method <- the hashing algorithm
Current Block Time <- blocktime as a float, 0 means none
Current Block Hash <- the hash of the current block, blank means none
LP <- whether LP is in use on at least 1 pool
Network Difficulty: the current network difficulty
"""
return await self.send_command("coin") return await self.send_command("coin")
async def debug(self, setting: str) -> dict: async def debug(self, setting: str) -> dict:
"""
API 'debug' command.
Returns which debug setting was enabled or disabled.
Parameters:
setting: which setting to switch to. Options are:
Silent,
Quiet,
Verbose,
Debug,
RPCProto,
PerDevice,
WorkTime,
Normal.
"""
return await self.send_command("debug", parameters=setting) return await self.send_command("debug", parameters=setting)
async def setconfig(self, name: str, n: int) -> dict: async def setconfig(self, name: str, n: int) -> dict:
"""
API 'setconfig' command.
Returns the STATUS section with the results of setting 'name' to N.
Parameters:
name: name of the config setting to set. Options are:
queue,
scantime,
expiry.
n: the value to set the 'name' setting to.
"""
return await self.send_command("setconfig", parameters=f"{name}, {n}") return await self.send_command("setconfig", parameters=f"{name}, {n}")
async def usbstats(self) -> dict: async def usbstats(self) -> dict:
"""
API 'usbstats' command.
Returns a dict containing the stats of all USB devices except ztex.
"""
return await self.send_command("usbstats") return await self.send_command("usbstats")
async def pgaset(self, n: int, opt: str, val: int = None) -> dict: async def pgaset(self, n: int, opt: str, val: int = None) -> dict:
"""
API 'pgaset' command.
Returns the STATUS section with the results of setting PGA N with opt[,val].
Parameters:
n: the PGA to set the options on.
opt: the option to set. Setting this to 'help' returns a help message.
val: the value to set the option to.
Options:
MMQ -
opt: clock
val: 160 - 230 (multiple of 2)
CMR -
opt: clock
val: 100 - 220
"""
if val: if val:
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}") return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
else: else:
return await self.send_command("pgaset", parameters=f"{n}, {opt}") return await self.send_command("pgaset", parameters=f"{n}, {opt}")
async def zero(self, which: str, value: bool) -> dict: async def zero(self, which: str, summary: bool) -> dict:
return await self.send_command("zero", parameters=f"{which}, {value}") """
API 'zero' command.
Returns the STATUS section with info on the zero and optional summary.
Parameters:
which: which device to zero.
Setting this to 'all' zeros all devices.
Setting this to 'bestshare' zeros only the bestshare values for each pool and global.
summary: whether or not to show a full summary.
"""
return await self.send_command("zero", parameters=f"{which}, {summary}")
async def hotplug(self, n: int) -> dict: async def hotplug(self, n: int) -> dict:
"""
API 'hotplug' command.
Returns the STATUS section with whether or not hotplug was enabled.
"""
return await self.send_command("hotplug", parameters=n) return await self.send_command("hotplug", parameters=n)
async def asc(self, n: int) -> dict: async def asc(self, n: int) -> dict:
"""
API 'asc' command.
Returns a dict containing the details of a single ASC of number N.
n: the ASC device to get details of.
"""
return await self.send_command("asc", parameters=n) return await self.send_command("asc", parameters=n)
async def ascenable(self, n: int) -> dict: async def ascenable(self, n: int) -> dict:
"""
API 'ascenable' command.
Returns the STATUS section with the results of enabling the ASC device N.
Parameters:
n: the number of the ASC to enable.
"""
return await self.send_command("ascenable", parameters=n) return await self.send_command("ascenable", parameters=n)
async def ascdisable(self, n: int) -> dict: async def ascdisable(self, n: int) -> dict:
"""
API 'ascdisable' command.
Returns the STATUS section with the results of disabling the ASC device N.
Parameters:
n: the number of the ASC to disable.
"""
return await self.send_command("ascdisable", parameters=n) return await self.send_command("ascdisable", parameters=n)
async def ascidentify(self, n: int) -> dict: async def ascidentify(self, n: int) -> dict:
"""
API 'ascidentify' command.
Returns the STATUS section with the results of identifying the ASC device N.
Parameters:
n: the number of the PGA to identify.
"""
return await self.send_command("ascidentify", parameters=n) return await self.send_command("ascidentify", parameters=n)
async def asccount(self) -> dict: async def asccount(self) -> dict:
"""
API 'asccount' command.
Returns a dict containing the number of ASC devices.
"""
return await self.send_command("asccount") return await self.send_command("asccount")
async def ascset(self, n: int, opt: str, val: int = None) -> dict: async def ascset(self, n: int, opt: str, val: int = None) -> dict:
"""
API 'ascset' command.
Returns the STATUS section with the results of setting ASC N with opt[,val].
Parameters:
n: the ASC to set the options on.
opt: the option to set. Setting this to 'help' returns a help message.
val: the value to set the option to.
Options:
AVA+BTB -
opt: freq
val: 256 - 1024 (chip frequency)
BTB -
opt: millivolts
val: 1000 - 1400 (core voltage)
MBA -
opt: reset
val: 0 - # of chips (reset a chip)
opt: freq
val: 0 - # of chips, 100 - 1400 (chip frequency)
opt: ledcount
val: 0 - 100 (chip count for LED)
opt: ledlimit
val: 0 - 200 (LED off below GH/s)
opt: spidelay
val: 0 - 9999 (SPI per I/O delay)
opt: spireset
val: i or s, 0 - 9999 (SPI regular reset)
opt: spisleep
val: 0 - 9999 (SPI reset sleep in ms)
BMA -
opt: volt
val: 0 - 9
opt: clock
val: 0 - 15
"""
if val: if val:
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}") return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
else: else:
return await self.send_command("ascset", parameters=f"{n}, {opt}") return await self.send_command("ascset", parameters=f"{n}, {opt}")
async def lcd(self) -> dict: async def lcd(self) -> dict:
"""
API 'lcd' command.
Returns a dict containing an all in one status summary of the miner.
"""
return await self.send_command("lcd") return await self.send_command("lcd")
async def lockstats(self) -> dict: async def lockstats(self) -> dict:
"""
API 'lockstats' command.
Returns the STATUS section with the result of writing the lock stats to STDERR.
"""
return await self.send_command("lockstats") return await self.send_command("lockstats")

44
CFG-Util-README.md Normal file
View File

@@ -0,0 +1,44 @@
# CFG-Util
## Interact with bitcoin mining ASICs using a simple GUI.
---
## Input Fields
### Network IP:
* Defaults to 192.168.1.0/24 (192.168.1.0 - 192.168.1.255)
* Enter any IP on your local network and it will automatically load your entire network with a /24 subnet (255 IP addresses)
* You can also add a subnet mask by adding a / after the IP and entering the subnet mask
* Press Scan to scan the selected network for miners
### IP List File:
* Use the Browse button to select a file
* Use the Import button to import all IP addresses from a file, regardless of where they are located in the file
* Use the Export button to export all IP addresses (or all selected IP addresses if you select some) to a file, with each seperated by a new line
### Config File:
* Use the Browse button to select a file
* Use the Import button to import the config file (only toml format is implemented right now)
* Use the Export button to export the config file in toml format
---
## Data Fields
### IP List:
* This field contains all the IP addresses of miners that were either imported from a file or scanned
* Select one by clicking, mutiple by holding CTRL and clicking, and select all between 2 chosen miners by holding SHIFT as you select them
* Use the ALL button to select all IP addresses in the field, or unselect all if they are selected
### Data:
* This field contains all data that is collected by selecting IP addresses and hitting GET
* The GET button gets data on all selected IP addresses
* The SORT IP button sorts the data list by IP address, as well as the IP List
* The SORT HR button sorts the data list by hashrate, as well as the IP List
* The SORT USER button sorts the data list by pool username, as well as the IP List
* The SORT W button sorts the data list by wattage, as well as the IP List
### Config:
* This field contains the configuration file either imported from a miner or from a file
* The IMPORT button imports the configuration file from any 1 selected miner to the config textbox
* The CONFIG button configures all selected miners with the config in the config textbox
* The LIGHT button turns on the fault light/locator light on miners that support it (Only BraiinsOS for now)
* The GENERATE button generates a new basic config in the config textbox

View File

@@ -21,6 +21,21 @@ if __name__ == '__main__':
2. Navigate to this directory, and run ```make_cfg_tool_exe.py build``` on Windows or ```python3 make_cfg_tool_exe.py``` on Mac or UNIX. 2. Navigate to this directory, and run ```make_cfg_tool_exe.py build``` on Windows or ```python3 make_cfg_tool_exe.py``` on Mac or UNIX.
### Interfacing with miners programmatically ### Interfacing with miners programmatically
<br>
##### Note: If you are trying to interface with Whatsminers, there is a bug in the way they are interacted with on Windows, so to fix that you need to change the event loop policy using this code:
```python
# need to import these 2 libraries, you need asyncio anyway so make sure you have sys imported
import sys
import asyncio
# if the computer is windows, set the event loop policy to a WindowsSelector policy
if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
```
##### It is likely a good idea to use this code in your program anyway to be preventative.
<br>
To write your own custom programs with this repo, you have many options. To write your own custom programs with this repo, you have many options.

View File

@@ -1,21 +1,17 @@
import asyncio
import ipaddress import ipaddress
import os import os
import re import re
import time import time
from operator import itemgetter
import asyncio
import aiofiles import aiofiles
import toml import toml
from cfg_util.miner_factory import miner_factory
from cfg_util.layout import window
from cfg_util.func.data import safe_parse_api_data
from config.bos import bos_config_convert, general_config_convert_bos
from API import APIError from API import APIError
from cfg_util.func.data import safe_parse_api_data
from cfg_util.layout import window
from cfg_util.miner_factory import miner_factory
from config.bos import bos_config_convert, general_config_convert_bos
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
@@ -36,10 +32,12 @@ async def update_prog_bar(amount):
async def set_progress_bar_len(amount): async def set_progress_bar_len(amount):
window["progress"].Update(0, max=amount) window["progress"].Update(0, max=amount)
window["progress"].maxlen = amount window["progress"].maxlen = amount
window["progress_percent"].Update("0.0 %")
async def scan_network(network): async def scan_network(network):
await update_ui_with_data("status", "Scanning") await update_ui_with_data("status", "Scanning")
await update_ui_with_data("hr_total", "")
network_size = len(network) network_size = len(network)
miner_generator = network.scan_network_generator() miner_generator = network.scan_network_generator()
await set_progress_bar_len(2 * network_size) await set_progress_bar_len(2 * network_size)
@@ -48,6 +46,10 @@ async def scan_network(network):
async for miner in miner_generator: async for miner in miner_generator:
if miner: if miner:
miners.append(miner) miners.append(miner)
# can output "Identifying" for each found item, but it gets a bit cluttered
# and could possibly be confusing for the end user because of timing on
# adding the IPs
# window["ip_table"].update([["Identifying...", "", "", "", ""] for miner in miners])
progress_bar_len += 1 progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len)) asyncio.create_task(update_prog_bar(progress_bar_len))
progress_bar_len += network_size - len(miners) progress_bar_len += network_size - len(miners)
@@ -56,10 +58,10 @@ async def scan_network(network):
all_miners = [] all_miners = []
async for found_miner in get_miner_genenerator: async for found_miner in get_miner_genenerator:
all_miners.append(found_miner) all_miners.append(found_miner)
all_miners.sort(key=lambda x: x.ip)
window["ip_table"].update([[str(miner.ip), "", "", "", ""] for miner in all_miners])
progress_bar_len += 1 progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len)) asyncio.create_task(update_prog_bar(progress_bar_len))
all_miners.sort(key=lambda x: x.ip)
window["ip_list"].update([str(miner.ip) for miner in all_miners])
await update_ui_with_data("ip_count", str(len(all_miners))) await update_ui_with_data("ip_count", str(len(all_miners)))
await update_ui_with_data("status", "") await update_ui_with_data("status", "")
@@ -69,21 +71,24 @@ async def miner_light(ips: list):
async def flip_light(ip): async def flip_light(ip):
listbox = window['ip_list'].Widget ip_list = window['ip_table'].Widget
miner = await miner_factory.get_miner(ip) miner = await miner_factory.get_miner(ip)
if ip in window["ip_list"].Values: index = [item[0] for item in window["ip_table"].Values].index(ip)
index = window["ip_list"].Values.index(ip) index_tags = ip_list.item(index)['tags']
if listbox.itemcget(index, "background") == 'red': if "light" not in index_tags:
listbox.itemconfigure(index, bg='#f0f3f7', fg='#000000') ip_list.item(index, tags=([*index_tags, "light"]))
await miner.fault_light_off() window['ip_table'].update(row_colors=[(index, "white", "red")])
else: await miner.fault_light_on()
listbox.itemconfigure(index, bg='red', fg='white') else:
await miner.fault_light_on() index_tags.remove("light")
ip_list.item(index, tags=index_tags)
window['ip_table'].update(row_colors=[(index, "black", "white")])
await miner.fault_light_off()
async def import_config(ip): async def import_config(idx):
await update_ui_with_data("status", "Importing") await update_ui_with_data("status", "Importing")
miner = await miner_factory.get_miner(ipaddress.ip_address(*ip)) miner = await miner_factory.get_miner(ipaddress.ip_address(window["ip_table"].Values[idx[0]][0]))
await miner.get_config() await miner.get_config()
config = miner.config config = miner.config
await update_ui_with_data("config", str(config)) await update_ui_with_data("config", str(config))
@@ -104,7 +109,7 @@ async def import_iplist(file_location):
if ip not in ip_list: if ip not in ip_list:
ip_list.append(ipaddress.ip_address(ip)) ip_list.append(ipaddress.ip_address(ip))
ip_list.sort() ip_list.sort()
window["ip_list"].update([str(ip) for ip in ip_list]) window["ip_table"].update([[str(ip), "", "", "", ""] for ip in ip_list])
await update_ui_with_data("ip_count", str(len(ip_list))) await update_ui_with_data("ip_count", str(len(ip_list)))
await update_ui_with_data("status", "") await update_ui_with_data("status", "")
@@ -120,8 +125,8 @@ async def export_iplist(file_location, ip_list_selected):
await file.write(str(item) + "\n") await file.write(str(item) + "\n")
else: else:
async with aiofiles.open(file_location, mode='w') as file: async with aiofiles.open(file_location, mode='w') as file:
for item in window['ip_list'].Values: for item in window['ip_table'].Values:
await file.write(str(item) + "\n") await file.write(str(item[0]) + "\n")
await update_ui_with_data("status", "") await update_ui_with_data("status", "")
@@ -183,27 +188,28 @@ async def export_config_file(file_location, config):
async def get_data(ip_list: list): async def get_data(ip_list: list):
await update_ui_with_data("status", "Getting Data") await update_ui_with_data("status", "Getting Data")
ips = [ipaddress.ip_address(ip) for ip in ip_list] ips = [ipaddress.ip_address(ip) for ip in ip_list]
if len(ips) == 0:
ips = [ipaddress.ip_address(ip) for ip in [item[0] for item in window["ip_table"].Values]]
await set_progress_bar_len(len(ips)) await set_progress_bar_len(len(ips))
progress_bar_len = 0 progress_bar_len = 0
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips]) data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
miner_data = [] ip_table_data = window["ip_table"].Values
ordered_all_ips = [item[0] for item in ip_table_data]
for all_data in data_gen: for all_data in data_gen:
miner_data.append(await all_data) data_point = await all_data
if data_point["IP"] in ordered_all_ips:
ip_table_index = ordered_all_ips.index(data_point["IP"])
ip_table_data[ip_table_index] = [
data_point["IP"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point['user'], str(data_point['wattage']) + " W"
]
window["ip_table"].update(ip_table_data)
progress_bar_len += 1 progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len)) asyncio.create_task(update_prog_bar(progress_bar_len))
miner_data.sort(key=lambda x: ipaddress.ip_address(x['IP'])) hashrate_list = [float(item[2].replace(" TH/s", "")) for item in window["ip_table"].Values]
total_hr = round(sum(hashrate_list), 2)
total_hr = round(sum(d.get('TH/s', 0) for d in miner_data), 2)
window["hr_total"].update(f"{total_hr} TH/s") window["hr_total"].update(f"{total_hr} TH/s")
window["hr_list"].update(disabled=False)
window["hr_list"].update([item['IP'] + " | "
+ item['host'] + " | "
+ str(item['TH/s']) + " TH/s | "
+ item['user'] + " | "
+ str(item['wattage']) + " W"
for item in miner_data])
window["hr_list"].update(disabled=True)
await update_ui_with_data("status", "") await update_ui_with_data("status", "")
@@ -224,7 +230,8 @@ async def get_formatted_data(ip: ipaddress.ip_address):
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS 5s') / 1000000, 2) th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS 5s') / 1000000, 2)
elif 'GHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys(): elif 'GHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
if not miner_data['summary'][0]['SUMMARY'][0]['GHS 5s'] == "": if not miner_data['summary'][0]['SUMMARY'][0]['GHS 5s'] == "":
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS 5s')) / 1000, 2) th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS 5s')) / 1000,
2)
else: else:
th5s = 0 th5s = 0
else: else:
@@ -240,21 +247,37 @@ async def get_formatted_data(ip: ipaddress.ip_address):
return {'TH/s': th5s, 'IP': str(miner.ip), 'host': host, 'user': user, 'wattage': wattage} return {'TH/s': th5s, 'IP': str(miner.ip), 'host': host, 'user': user, 'wattage': wattage}
async def generate_config(): async def generate_config(username, workername, v2_allowed):
if username and workername:
user = f"{username}.{workername}"
elif username and not workername:
user = username
else:
return
if v2_allowed:
url_1 = 'stratum2+tcp://v2.us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt'
url_2 = 'stratum2+tcp://v2.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt'
url_3 = 'stratum+tcp://stratum.slushpool.com:3333'
else:
url_1 = 'stratum+tcp://ca.stratum.slushpool.com:3333'
url_2 = 'stratum+tcp://us-east.stratum.slushpool.com:3333'
url_3 = 'stratum+tcp://stratum.slushpool.com:3333'
config = {'group': [{ config = {'group': [{
'name': 'group', 'name': 'group',
'quota': 1, 'quota': 1,
'pool': [{ 'pool': [{
'url': 'stratum2+tcp://us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt', 'url': url_1,
'user': 'UpstreamDataInc.test', 'user': user,
'password': '123' 'password': '123'
}, { }, {
'url': 'stratum2+tcp://stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt', 'url': url_2,
'user': 'UpstreamDataInc.test', 'user': user,
'password': '123' 'password': '123'
}, { }, {
'url': 'stratum+tcp://stratum.slushpool.com:3333', 'url': url_3,
'user': 'UpstreamDataInc.test', 'user': user,
'password': '123' 'password': '123'
}] }]
}], }],
@@ -279,45 +302,23 @@ async def generate_config():
async def sort_data(index: int or str): async def sort_data(index: int or str):
await update_ui_with_data("status", "Sorting Data") await update_ui_with_data("status", "Sorting Data")
data_list = window['hr_list'].Values data_list = window['ip_table'].Values
new_list = []
indexes = {} # wattage
for item in data_list: if re.match("[0-9]* W", data_list[0][index]):
item_data = [part.strip() for part in item.split("|")] new_list = sorted(data_list, key=lambda x: int(x[index].replace(" W", "")))
for idx, part in enumerate(item_data):
if re.match("[0-9]* W", part): # hashrate
item_data[idx] = item_data[idx].replace(" W", "") elif re.match("[0-9]*\.?[0-9]* TH\/s", data_list[0][index]):
indexes['wattage'] = idx new_list = sorted(data_list, key=lambda x: float(x[index].replace(" TH/s", "")))
elif re.match("[0-9]*\.?[0-9]* TH\/s", part):
item_data[idx] = item_data[idx].replace(" TH/s", "") # ip addresses
indexes['hr'] = idx elif re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",
elif re.match("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)", part): data_list[0][index]):
item_data[idx] = ipaddress.ip_address(item_data[idx]) new_list = sorted(data_list, key=lambda x: ipaddress.ip_address(x[index]))
indexes['ip'] = idx
new_list.append(item_data) # everything else, hostname and user
if not isinstance(index, str):
if index == indexes['hr']:
new_data_list = sorted(new_list, key=lambda x: float(x[index]))
else:
new_data_list = sorted(new_list, key=itemgetter(index))
else: else:
if index.lower() not in indexes.keys(): new_list = sorted(data_list, key=lambda x: x[index])
return await update_ui_with_data("ip_table", new_list)
elif index.lower() == 'hr':
new_data_list = sorted(new_list, key=lambda x: float(x[indexes[index]]))
else:
new_data_list = sorted(new_list, key=itemgetter(indexes[index]))
new_ip_list = []
for item in new_data_list:
new_ip_list.append(item[indexes['ip']])
new_data_list = [str(item[indexes['ip']]) + " | "
+ item[1] + " | "
+ item[indexes['hr']] + " TH/s | "
+ item[3] + " | "
+ str(item[indexes['wattage']]) + " W"
for item in new_data_list]
window["hr_list"].update(disabled=False)
window["hr_list"].update(new_data_list)
window['ip_list'].update(new_ip_list)
window["hr_list"].update(disabled=True)
await update_ui_with_data("status", "") await update_ui_with_data("status", "")

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,7 @@
"""
This file stores the MinerFactory instance used by the ConfigUtility for use in other files.
"""
from miners.miner_factory import MinerFactory from miners.miner_factory import MinerFactory
miner_factory = MinerFactory() miner_factory = MinerFactory()

View File

@@ -1,7 +1,8 @@
import asyncio import asyncio
import sys import sys
import PySimpleGUI as sg
from cfg_util.layout import window from cfg_util.layout import window, generate_config_layout
from cfg_util.func import scan_network, sort_data, send_config, miner_light, get_data, export_config_file, \ from cfg_util.func import scan_network, sort_data, send_config, miner_light, get_data, export_config_file, \
generate_config, import_config, import_iplist, import_config_file, export_iplist generate_config, import_config, import_iplist, import_config_file, export_iplist
@@ -11,7 +12,7 @@ from network import MinerNetwork
async def ui(): async def ui():
while True: while True:
event, value = window.read(timeout=10) event, value = window.read(timeout=10)
if event in (None, 'Close'): if event in (None, 'Close', sg.WIN_CLOSED):
sys.exit() sys.exit()
if event == 'scan': if event == 'scan':
if len(value['miner_network'].split("/")) > 1: if len(value['miner_network'].split("/")) > 1:
@@ -21,36 +22,51 @@ async def ui():
miner_network = MinerNetwork(value['miner_network']) miner_network = MinerNetwork(value['miner_network'])
asyncio.create_task(scan_network(miner_network)) asyncio.create_task(scan_network(miner_network))
if event == 'select_all_ips': if event == 'select_all_ips':
if value['ip_list'] == window['ip_list'].Values: if len(value["ip_table"]) == len(window["ip_table"].Values):
window['ip_list'].set_value([]) window["ip_table"].update(select_rows=())
else: else:
window['ip_list'].set_value(window['ip_list'].Values) window["ip_table"].update(select_rows=([row for row in range(len(window["ip_table"].Values))]))
if event == 'import_config': if event == 'import_config':
if 2 > len(value['ip_list']) > 0: if 2 > len(value['ip_table']) > 0:
asyncio.create_task(import_config(value['ip_list'])) asyncio.create_task(import_config(value['ip_table']))
if event == 'light': if event == 'light':
asyncio.create_task(miner_light(value['ip_list'])) asyncio.create_task(miner_light([window['ip_table'].Values[item][0] for item in value['ip_table']]))
if event == "import_iplist": if event == "import_iplist":
asyncio.create_task(import_iplist(value["file_iplist"])) asyncio.create_task(import_iplist(value["file_iplist"]))
if event == "export_iplist": if event == "export_iplist":
asyncio.create_task(export_iplist(value["file_iplist"], value['ip_list'])) asyncio.create_task(export_iplist(value["file_iplist"], [window['ip_table'].Values[item][0] for item in value['ip_table']]))
if event == "send_config": if event == "send_config":
asyncio.create_task(send_config(value['ip_list'], value['config'])) asyncio.create_task(send_config([window['ip_table'].Values[item][0] for item in value['ip_table']], value['config']))
if event == "import_file_config": if event == "import_file_config":
asyncio.create_task(import_config_file(value['file_config'])) asyncio.create_task(import_config_file(value['file_config']))
if event == "export_file_config": if event == "export_file_config":
asyncio.create_task(export_config_file(value['file_config'], value["config"])) asyncio.create_task(export_config_file(value['file_config'], value["config"]))
if event == "get_data": if event == "get_data":
asyncio.create_task(get_data(value['ip_list'])) asyncio.create_task(get_data([window["ip_table"].Values[item][0] for item in value["ip_table"]]))
if event == "generate_config": if event == "generate_config":
asyncio.create_task(generate_config()) await generate_config_ui()
if event == "sort_data_ip": if event == "sort_data_ip":
asyncio.create_task(sort_data('ip')) asyncio.create_task(sort_data(0)) # ip index in table
if event == "sort_data_hr": if event == "sort_data_hr":
asyncio.create_task(sort_data('hr')) asyncio.create_task(sort_data(2)) # HR index in table
if event == "sort_data_user": if event == "sort_data_user":
asyncio.create_task(sort_data(3)) asyncio.create_task(sort_data(3)) # user index in table
if event == "sort_data_w": if event == "sort_data_w":
asyncio.create_task(sort_data('wattage')) asyncio.create_task(sort_data(4)) # wattage index in table
if event == "__TIMEOUT__": if event == "__TIMEOUT__":
await asyncio.sleep(0) await asyncio.sleep(0)
async def generate_config_ui():
generate_config_window = sg.Window("Generate Config", generate_config_layout(), modal=True)
while True:
event, values = generate_config_window.read()
if event in (None, 'Close', sg.WIN_CLOSED):
break
if event == "generate_config_window_generate":
if values['generate_config_window_username']:
await generate_config(values['generate_config_window_username'],
values['generate_config_window_workername'],
values['generate_config_window_allow_v2'])
generate_config_window.close()
break

View File

@@ -8,7 +8,6 @@ The build will show up in the build directory.
import datetime import datetime
import sys import sys
import os import os
from cx_Freeze import setup, Executable from cx_Freeze import setup, Executable
base = None base = None
@@ -18,9 +17,15 @@ if sys.platform == "win32":
version = datetime.datetime.now() version = datetime.datetime.now()
version = version.strftime("%y.%m.%d") version = version.strftime("%y.%m.%d")
print(version) print(version)
setup(name="UpstreamCFGUtil.exe", setup(name="UpstreamCFGUtil.exe",
version=version, version=version,
description="Upstream Data Config Utility Build", description="Upstream Data Config Utility Build",
options={"build_exe": {"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\"}}, options={"build_exe": {"build_exe": f"{os.getcwd()}\\build\\UpstreamCFGUtil-{version}-{sys.platform}\\",
"include_files": [os.path.join(os.getcwd(), "settings.toml"),
os.path.join(os.getcwd(), "CFG-Util-README.md")],
},
},
executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")] executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")]
) )

View File

@@ -16,6 +16,14 @@ class MinerFactory:
self.miners = {} self.miners = {}
async def get_miner_generator(self, ips: list): async def get_miner_generator(self, ips: list):
"""
Get Miner objects from ip addresses using an async generator.
Returns an asynchronous generator containing Miners.
Parameters:
ips: a list of ip addresses to get miners for.
"""
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
scan_tasks = [] scan_tasks = []
for miner in ips: for miner in ips:

View File

@@ -31,7 +31,7 @@ class MinerNetwork:
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False) return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
async def scan_network_for_miners(self) -> None or list: async def scan_network_for_miners(self) -> None or list:
"""Scan the network for miners, and """ """Scan the network for miners, and return found miners as a list."""
local_network = self.get_network() local_network = self.get_network()
print(f"Scanning {local_network} for miners...") print(f"Scanning {local_network} for miners...")
scan_tasks = [] scan_tasks = []
@@ -55,6 +55,11 @@ class MinerNetwork:
return miners return miners
async def scan_network_generator(self): async def scan_network_generator(self):
"""
Scan the network for miners using an async generator.
Returns an asynchronous generator containing found miners.
"""
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
local_network = self.get_network() local_network = self.get_network()
scan_tasks = [] scan_tasks = []

Binary file not shown.