Compare commits

..

40 Commits

Author SHA1 Message Date
UpstreamData
88539650ca updated CFG-Util-README.md to be correct 2022-01-07 11:06:08 -07:00
UpstreamData
3cf0162892 fixed some bugs and ignored APIWarnings when getting data with the GUI 2022-01-07 10:55:02 -07:00
UpstreamData
51e9e19409 add S19 and S17 and S9 models to GUI 2022-01-07 10:44:09 -07:00
UpstreamData
c93d99b27c updated the gui to get the model 2022-01-07 10:35:25 -07:00
UpstreamData
770b17c86b added get_model to X19s 2022-01-07 10:25:29 -07:00
UpstreamData
4e8ff9ea74 added btminer get_model and improved return on the rest of the get_models 2022-01-07 10:20:55 -07:00
UpstreamData
8ec8c57e31 added get_model to get the model of the miner, and reformatted the style of the miner factory getting miner to get a different miner for each type of supported miner 2022-01-07 10:08:20 -07:00
UpstreamData
48aa7232b1 added actual miners versions and types to the factory 2022-01-07 09:34:05 -07:00
UpstreamData
1f3ffe96a1 refactored files and folders 2022-01-06 14:48:11 -07:00
UpstreamData
e0505a31ca fixed a bug with the total miner count not showing when getting data and scanning together 2022-01-06 13:19:31 -07:00
UpstreamData
3ecc27b3f9 added the ability to copy a list of IP addresses directly from the table. 2022-01-06 13:15:08 -07:00
UpstreamData
5d66c539d4 fixed a bug with whatsminer temps 2022-01-06 12:46:34 -07:00
UpstreamData
c751d53398 added warnings to notify when removing 2022-01-06 12:37:43 -07:00
UpstreamData
ea1e8abeac Merge remote-tracking branch 'origin/master' 2022-01-06 11:19:09 -07:00
UpstreamData
8d3f6a3c06 fixed a bug with getting data and scanning where the progress bar would not update if no miners were found 2022-01-06 11:16:53 -07:00
UpstreamData
f35adf0ae4 Delete custom.md 2022-01-06 10:42:24 -07:00
UpstreamData
848ac6ef7c Update issue templates 2022-01-06 10:41:39 -07:00
UpstreamData
6db7cd4a1f added X19 temp support 2022-01-06 10:12:18 -07:00
UpstreamData
23d465a733 clicking get data with no ip addresses scanned now scans the network then gets data 2022-01-05 15:54:15 -07:00
UpstreamData
1148946a29 added temperatures to the tool, and fixed a bug with multicommand not removing bad commands if they were adjacent to each other in the list 2022-01-05 15:33:56 -07:00
UpstreamData
e77cbc5415 added a bidirectional sort on table headers and changed to an "Open in web" button to make it less convoluted and buggy to sort the table 2022-01-05 14:00:36 -07:00
UpstreamData
5ecb87ec63 added the option to sort using the table headers and added a double click on the miners to open a web browser with that miner 2022-01-05 13:45:34 -07:00
UpstreamData
8ef135dfd7 reformatted and fixed a bunch of small formatting related issues 2022-01-05 13:00:55 -07:00
UpstreamData
c26a2cc99e added wattage for whatsminers when scanning 2022-01-05 11:58:42 -07:00
UpstreamData
e0d8078bf1 fixed a small bug with the ip table not resetting at the start of the scan 2022-01-05 11:22:02 -07:00
UpstreamData
4528060fd0 fixed a bug with the way the hashrate total works when getting new data on a small subset of miners 2022-01-05 10:39:36 -07:00
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
44 changed files with 1823 additions and 884 deletions

37
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,37 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
**Miner Information (If applicable):**
- Manufacturer: [e.g. Bitmain, MicroBT]
- Type: [e.g. S9, M20]
- Firmware Type: [e.g. Stock, BraiinsOS]
- Firmware Version:
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,6 +1,7 @@
import asyncio
import json
import ipaddress
import warnings
class APIError(Exception):
@@ -17,6 +18,20 @@ class APIError(Exception):
return "Incorrect API parameters."
class APIWarning(Warning):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Incorrect API parameters."
class BaseMinerAPI:
def __init__(self, ip: str, port: int = 4028) -> None:
# api port, should be 4028
@@ -41,19 +56,33 @@ class BaseMinerAPI:
async def multicommand(self, *commands: str) -> dict:
"""Creates and sends multiple commands as one command to the miner."""
# split the commands into a proper list
commands = [*commands]
for item in commands:
user_commands = [*commands]
allowed_commands = self.get_commands()
# make sure we can actually run the command, otherwise it will fail
if item not in self.get_commands():
# if the command isnt allowed, remove it
print(f"Removing incorrect command: {item}")
commands.remove(item)
commands = [command for command in user_commands if command in allowed_commands]
for item in list(set(user_commands) - set(commands)):
warnings.warn(f"""Removing incorrect command: {item}
If you are sure you want to use this command please use API.send_command("{item}") instead.""",
APIWarning)
# standard multicommand format is "command1+command2"
# doesnt work for S19 which is dealt with in the send command function
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:
"""Send an API command to the miner and return the result."""
@@ -94,26 +123,15 @@ class BaseMinerAPI:
writer.close()
await writer.wait_closed()
# 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):
raise APIError(data["STATUS"][0]["Msg"])
# validate the command succeeded
validation = self.validate_command_output(data)
if not validation[0]:
raise APIError(validation[1])
return data
@staticmethod
def validate_command_output(data: dict) -> bool:
def validate_command_output(data: dict) -> tuple[bool, str | None]:
"""Check if the returned command output is correctly formatted."""
# check if the data returned is correct or an error
# if status isn't a key, it is a multicommand
@@ -122,20 +140,20 @@ class BaseMinerAPI:
# make sure not to try to turn id into a dict
if not key == "id":
# make sure they succeeded
if "STATUS" in data.keys():
if "STATUS" in data[key][0].keys():
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
# this is an error
return False
return False, f"{key}: " + data[key][0]["STATUS"][0]["Msg"]
elif "id" not in data.keys():
if data["STATUS"] not in ["S", "I"]:
return False
return False, data["Msg"]
else:
# make sure the command succeeded
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error
if data["STATUS"][0]["STATUS"] not in ("S", "I"):
return False
return True
return False, data["STATUS"][0]["Msg"]
return True, None
@staticmethod
def load_api_data(data: bytes) -> dict:

View File

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

View File

@@ -2,49 +2,143 @@ from API import 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):
super().__init__(ip, port)
async def asccount(self) -> dict:
"""
API 'asccount' command.
Returns a dict containing the number of ASC devices.
"""
return await self.send_command("asccount")
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)
async def devdetails(self) -> dict:
"""
API 'devdetails' command.
Returns a dict containing all devices with their static details.
"""
return await self.send_command("devdetails")
async def devs(self) -> dict:
"""
API 'devs' command.
Returns a dict containing each PGA/ASC with their details.
"""
return await self.send_command("devs")
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:
return await self.send_command("edevs", parameters="old")
else:
return await self.send_command("edevs")
async def pools(self) -> dict:
"""
API 'pools' command.
Returns a dict containing the status of each pool.
"""
return await self.send_command("pools")
async def summary(self) -> dict:
"""
API 'summary' command.
Returns a dict containing the status summary of the miner.
"""
return await self.send_command("summary")
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")
async def version(self) -> dict:
"""
API 'version' command.
Returns a dict containing version information.
"""
return await self.send_command("version")
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.
"""
return await self.send_command("estats")
async def check(self) -> dict:
return await self.send_command("check")
async def check(self, command: str) -> 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)
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")
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")
async def switchpool(self, n: int) -> dict:
@@ -73,19 +167,53 @@ class BOSMinerAPI(BaseMinerAPI):
# return await self.send_command("removepool", parameters=n)
async def fans(self) -> dict:
"""
API 'fans' command.
Returns a dict containing information on fans and fan speeds.
"""
return await self.send_command("fans")
async def tempctrl(self) -> dict:
"""
API 'tempctrl' command.
Returns a dict containing temp control configuration.
"""
return await self.send_command("tempctrl")
async def temps(self) -> dict:
"""
API 'temps' command.
Returns a dict containing temperature information.
"""
return await self.send_command("temps")
async def tunerstatus(self) -> dict:
"""
API 'tunerstatus' command.
Returns a dict containing tuning stats.
"""
return await self.send_command("tunerstatus")
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")
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")

View File

@@ -1,12 +1,12 @@
from API import BaseMinerAPI, APIError
from passlib.hash import md5_crypt
from passlib.handlers.md5_crypt import md5_crypt
import asyncio
import re
import json
import hashlib
import binascii
from Crypto.Cipher import AES
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import base64
@@ -42,13 +42,19 @@ def _add_to_16(s: str) -> bytes:
def parse_btminer_priviledge_data(token_data, data):
# get the encoded data from the dict
enc_data = data['enc']
# get the aes key from the token data
aeskey = hashlib.sha256(token_data['host_passwd_md5'].encode()).hexdigest()
# unhexlify the aes key
aeskey = binascii.unhexlify(aeskey.encode())
aes = AES.new(aeskey, AES.MODE_ECB)
ret_msg = json.loads(str(
aes.decrypt(base64.decodebytes(bytes(
enc_data, encoding='utf8'))).rstrip(b'\0').decode("utf8")))
# create the required decryptor
aes = Cipher(algorithms.AES(aeskey), modes.ECB())
decryptor = aes.decryptor()
# 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
@@ -60,13 +66,12 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes:
# unhexlify the encoded host_passwd
aeskey = binascii.unhexlify(aeskey.encode())
# 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
api_json_str = json.dumps(command)
# encode the json command with the aes key
api_json_str_enc = str(base64.encodebytes(
aes.encrypt(_add_to_16(api_json_str))),
encoding='utf8').replace('\n', '')
api_json_str_enc = base64.encodebytes(encryptor.update(_add_to_16(api_json_str))).decode("utf-8").replace("\n", "")
# label the data as being encoded
data_enc = {'enc': 1, 'data': api_json_str_enc}
# dump the labeled data to json
@@ -82,7 +87,9 @@ class BTMinerAPI(BaseMinerAPI):
async def send_command(self, command: str | bytes, **kwargs) -> dict:
"""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 it is a string, put it into the standard command format
command = json.dumps({"command": command}).encode("utf-8")
try:
# get reader and writer streams
@@ -116,18 +123,28 @@ class BTMinerAPI(BaseMinerAPI):
writer.close()
await writer.wait_closed()
# check if th returned data is encoded
if 'enc' in data.keys():
# try to parse the encoded data
try:
data = parse_btminer_priviledge_data(self.current_token, data)
except Exception as e:
print(e)
if not self.validate_command_output(data):
raise APIError(data["Msg"])
# if it fails to validate, it is likely an error
validation = self.validate_command_output(data)
if not validation[0]:
raise APIError(validation[1])
# return the parsed json as a dict
return data
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")
pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$')
pwd = pwd.split('$')
@@ -138,7 +155,7 @@ class BTMinerAPI(BaseMinerAPI):
self.current_token = {'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
# how to configure the whatsminer API to
# use these commands.
@@ -147,8 +164,13 @@ class BTMinerAPI(BaseMinerAPI):
pool_1: str, worker_1: str, passwd_1: str,
pool_2: str = None, worker_2: str = None, passwd_2: 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()
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 = {
"cmd": "update_pools",
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
@@ -166,16 +188,33 @@ class BTMinerAPI(BaseMinerAPI):
"cmd": "update_pools",
"pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1,
}
# encode the command with the token data
enc_command = create_privileged_cmd(token_data, command)
# send the 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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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:
command = {"cmd": "power_off", "respbefore": "true"}
else:
@@ -185,24 +224,58 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, 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):
"""
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}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
@@ -214,18 +287,43 @@ class BTMinerAPI(BaseMinerAPI):
return NotImplementedError
async def reboot(self):
"""
API 'reboot' command.
Reboots the miner.
Returns the status of the command then reboots.
"""
command = {"cmd": "reboot"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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
if len(new_pwd.encode('utf-8')) > 8:
return APIError(
@@ -236,6 +334,13 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command)
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:
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)}
@@ -244,36 +349,82 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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"}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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}
token_data = await self.get_token()
enc_command = create_privileged_cmd(token_data, command)
return await self.send_command(enc_command)
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:
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)}
@@ -282,6 +433,18 @@ class BTMinerAPI(BaseMinerAPI):
return await self.send_command(enc_command)
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":
return APIError(
'Message is incorrect, please choose one of '
@@ -299,28 +462,82 @@ class BTMinerAPI(BaseMinerAPI):
#### END privileged COMMANDS ####
async def summary(self):
"""
API 'summary' command.
Returns a dict containing the status summary of the miner.
"""
return await self.send_command("summary")
async def pools(self):
"""
API 'pools' command.
Returns a dict containing the status of each pool.
"""
return await self.send_command("pools")
async def devs(self):
"""
API 'devs' command.
Returns a dict containing each PGA/ASC with their details.
"""
return await self.send_command("devs")
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")
async def devdetails(self):
"""
API 'devdetails' command.
Returns a dict containing all devices with their static details.
"""
return await self.send_command("devdetails")
async def get_psu(self):
"""
API 'get_psu' command.
Returns a dict containing PSU and power information.
"""
return await self.send_command("get_psu")
async def version(self):
"""
API 'get_version' command.
Returns a dict containing version information.
"""
return await self.get_version()
async def get_version(self):
"""
API 'get_version' command.
Returns a dict containing version information.
"""
return await self.send_command("get_version")
async def status(self):
"""
API 'status' command.
Returns a dict containing BTMiner status and firmware version.
"""
return await self.send_command("status")
async def get_miner_info(self):
"""
API 'get_miner_info' command.
Returns a dict containing general miner info.
"""
return await self.send_command("get_miner_info")

View File

@@ -2,6 +2,18 @@ from API import 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):
super().__init__(ip, port)
@@ -29,139 +41,492 @@ class CGMinerAPI(BaseMinerAPI):
return await self.send_command("config")
async def summary(self) -> dict:
"""
API 'summary' command.
Returns a dict containing the status summary of the miner.
"""
return await self.send_command("summary")
async def pools(self) -> dict:
"""
API 'pools' command.
Returns a dict containing the status of each pool.
"""
return await self.send_command("pools")
async def devs(self) -> dict:
"""
API 'devs' command.
Returns a dict containing each PGA/ASC with their details.
"""
return await self.send_command("devs")
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:
return await self.send_command("edevs", parameters="old")
else:
return await self.send_command("edevs")
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)
async def pgacount(self) -> dict:
"""
API 'pgacount' command.
Returns a dict containing the number of PGA devices.
"""
return await self.send_command("pgacount")
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)
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)
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}")
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])}")
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}")
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)
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)
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:
return await self.send_command("save", parameters=filename)
else:
return await self.send_command("save")
async def quit(self) -> dict:
"""
API 'quit' command.
Returns a single "BYE" before CGMiner quits.
"""
return await self.send_command("quit")
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")
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")
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)
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)
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)
async def devdetails(self) -> dict:
"""
API 'devdetails' command.
Returns a dict containing all devices with their static details.
"""
return await self.send_command("devdetails")
async def restart(self) -> dict:
"""
API 'restart' command.
Returns a single "RESTART" before CGMiner restarts.
"""
return await self.send_command("restart")
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")
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:
return await self.send_command("estats", parameters="old")
else:
return await self.send_command("estats")
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)
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)
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")
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)
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}")
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")
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:
return await self.send_command("pgaset", parameters=f"{n}, {opt}, {val}")
else:
return await self.send_command("pgaset", parameters=f"{n}, {opt}")
async def zero(self, which: str, value: bool) -> dict:
return await self.send_command("zero", parameters=f"{which}, {value}")
async def zero(self, which: str, summary: bool) -> dict:
"""
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:
"""
API 'hotplug' command.
Returns the STATUS section with whether or not hotplug was enabled.
"""
return await self.send_command("hotplug", parameters=n)
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)
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)
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)
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)
async def asccount(self) -> dict:
"""
API 'asccount' command.
Returns a dict containing the number of ASC devices.
"""
return await self.send_command("asccount")
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:
return await self.send_command("ascset", parameters=f"{n}, {opt}, {val}")
else:
return await self.send_command("ascset", parameters=f"{n}, {opt}")
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")
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")

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.
### 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.
@@ -31,7 +46,7 @@ A basic script to find all miners on the network and get the hashrate from them
```python
import asyncio
from network import MinerNetwork
from cfg_util.func import safe_parse_api_data
from cfg_util.func.parse_data import safe_parse_api_data
async def get_hashrate():
# Miner Network class allows for easy scanning of a network
@@ -65,7 +80,7 @@ You can also create your own miner without scanning if you know the IP:
import asyncio
import ipaddress
from miners.miner_factory import MinerFactory
from cfg_util.func import safe_parse_api_data
from cfg_util.func.parse_data import safe_parse_api_data
async def get_miner_hashrate(ip: str):
# Instantiate a Miner Factory to generate miners from their IP
@@ -91,7 +106,7 @@ Or generate a miner directly without the factory:
```python
import asyncio
from miners.bosminer import BOSminer
from cfg_util.func import safe_parse_api_data
from cfg_util.func.parse_data import safe_parse_api_data
async def get_miner_hashrate(ip: str):
# Create a BOSminer miner object
@@ -113,7 +128,7 @@ Or finally, just get the API directly:
```python
import asyncio
from API.bosminer import BOSMinerAPI
from cfg_util.func import safe_parse_api_data
from cfg_util.func.parse_data import safe_parse_api_data
async def get_miner_hashrate(ip: str):
# Create a BOSminerAPI object

View File

@@ -1,323 +0,0 @@
import ipaddress
import os
import re
import time
from operator import itemgetter
import asyncio
import aiofiles
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 settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
async def update_ui_with_data(key, message, append=False):
if append:
message = window[key].get_text() + message
window[key].update(message)
async def update_prog_bar(amount):
window["progress"].Update(amount)
percent_done = 100 * (amount / window['progress'].maxlen)
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
if percent_done == 100:
window["progress_percent"].Update("")
async def set_progress_bar_len(amount):
window["progress"].Update(0, max=amount)
window["progress"].maxlen = amount
async def scan_network(network):
await update_ui_with_data("status", "Scanning")
network_size = len(network)
miner_generator = network.scan_network_generator()
await set_progress_bar_len(2 * network_size)
progress_bar_len = 0
miners = []
async for miner in miner_generator:
if miner:
miners.append(miner)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
progress_bar_len += network_size - len(miners)
asyncio.create_task(update_prog_bar(progress_bar_len))
get_miner_genenerator = miner_factory.get_miner_generator(miners)
all_miners = []
async for found_miner in get_miner_genenerator:
all_miners.append(found_miner)
progress_bar_len += 1
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("status", "")
async def miner_light(ips: list):
await asyncio.gather(*[flip_light(ip) for ip in ips])
async def flip_light(ip):
listbox = window['ip_list'].Widget
miner = await miner_factory.get_miner(ip)
if ip in window["ip_list"].Values:
index = window["ip_list"].Values.index(ip)
if listbox.itemcget(index, "background") == 'red':
listbox.itemconfigure(index, bg='#f0f3f7', fg='#000000')
await miner.fault_light_off()
else:
listbox.itemconfigure(index, bg='red', fg='white')
await miner.fault_light_on()
async def import_config(ip):
await update_ui_with_data("status", "Importing")
miner = await miner_factory.get_miner(ipaddress.ip_address(*ip))
await miner.get_config()
config = miner.config
await update_ui_with_data("config", str(config))
await update_ui_with_data("status", "")
async def import_iplist(file_location):
await update_ui_with_data("status", "Importing")
if not os.path.exists(file_location):
return
else:
ip_list = []
async with aiofiles.open(file_location, mode='r') as file:
async for line in file:
ips = [x.group() for x in re.finditer(
"^((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]?)", line)]
for ip in ips:
if ip not in ip_list:
ip_list.append(ipaddress.ip_address(ip))
ip_list.sort()
window["ip_list"].update([str(ip) for ip in ip_list])
await update_ui_with_data("ip_count", str(len(ip_list)))
await update_ui_with_data("status", "")
async def export_iplist(file_location, ip_list_selected):
await update_ui_with_data("status", "Exporting")
if not os.path.exists(file_location):
return
else:
if ip_list_selected is not None and not ip_list_selected == []:
async with aiofiles.open(file_location, mode='w') as file:
for item in ip_list_selected:
await file.write(str(item) + "\n")
else:
async with aiofiles.open(file_location, mode='w') as file:
for item in window['ip_list'].Values:
await file.write(str(item) + "\n")
await update_ui_with_data("status", "")
async def send_config_generator(miners: list, config):
loop = asyncio.get_event_loop()
config_tasks = []
for miner in miners:
if len(config_tasks) >= CONFIG_THREADS:
configured = asyncio.as_completed(config_tasks)
config_tasks = []
for sent_config in configured:
yield await sent_config
config_tasks.append(loop.create_task(miner.send_config(config)))
configured = asyncio.as_completed(config_tasks)
for sent_config in configured:
yield await sent_config
async def send_config(ips: list, config):
await update_ui_with_data("status", "Configuring")
await set_progress_bar_len(2 * len(ips))
progress_bar_len = 0
get_miner_genenerator = miner_factory.get_miner_generator(ips)
all_miners = []
async for miner in get_miner_genenerator:
all_miners.append(miner)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
config_sender_generator = send_config_generator(all_miners, config)
async for _config_sender in config_sender_generator:
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
await update_ui_with_data("status", "")
async def import_config_file(file_location):
await update_ui_with_data("status", "Importing")
if not os.path.exists(file_location):
return
else:
async with aiofiles.open(file_location, mode='r') as file:
config = await file.read()
await update_ui_with_data("config", await bos_config_convert(toml.loads(config)))
await update_ui_with_data("status", "")
async def export_config_file(file_location, config):
await update_ui_with_data("status", "Exporting")
config = toml.loads(config)
config['format']['generator'] = 'upstream_config_util'
config['format']['timestamp'] = int(time.time())
config = toml.dumps(config)
async with aiofiles.open(file_location, mode='w+') as file:
await file.write(await general_config_convert_bos(config))
await update_ui_with_data("status", "")
async def get_data(ip_list: list):
await update_ui_with_data("status", "Getting Data")
ips = [ipaddress.ip_address(ip) for ip in ip_list]
await set_progress_bar_len(len(ips))
progress_bar_len = 0
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
miner_data = []
for all_data in data_gen:
miner_data.append(await all_data)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
miner_data.sort(key=lambda x: ipaddress.ip_address(x['IP']))
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_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", "")
async def get_formatted_data(ip: ipaddress.ip_address):
miner = await miner_factory.get_miner(ip)
try:
miner_data = await miner.api.multicommand("summary", "pools", "tunerstatus")
except APIError:
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
host = await miner.get_hostname()
if "tunerstatus" in miner_data.keys():
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
# data['tunerstatus'][0]['TUNERSTATUS'][0]['PowerLimit']
else:
wattage = 0
if "summary" in miner_data.keys():
if 'MHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
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():
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)
else:
th5s = 0
else:
th5s = 0
else:
th5s = 0
if "pools" not in miner_data.keys():
user = "?"
elif not miner_data['pools'][0]['POOLS'] == []:
user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User')
else:
user = "Blank"
return {'TH/s': th5s, 'IP': str(miner.ip), 'host': host, 'user': user, 'wattage': wattage}
async def generate_config():
config = {'group': [{
'name': 'group',
'quota': 1,
'pool': [{
'url': 'stratum2+tcp://us-east.stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt',
'user': 'UpstreamDataInc.test',
'password': '123'
}, {
'url': 'stratum2+tcp://stratum.slushpool.com/u95GEReVMjK6k5YqiSFNqqTnKU4ypU2Wm8awa6tmbmDmk1bWt',
'user': 'UpstreamDataInc.test',
'password': '123'
}, {
'url': 'stratum+tcp://stratum.slushpool.com:3333',
'user': 'UpstreamDataInc.test',
'password': '123'
}]
}],
'format': {
'version': '1.2+',
'model': 'Antminer S9',
'generator': 'upstream_config_util',
'timestamp': int(time.time())
},
'temp_control': {
'target_temp': 80.0,
'hot_temp': 90.0,
'dangerous_temp': 120.0
},
'autotuning': {
'enabled': True,
'psu_power_limit': 900
}
}
window['config'].update(await bos_config_convert(config))
async def sort_data(index: int or str):
await update_ui_with_data("status", "Sorting Data")
data_list = window['hr_list'].Values
new_list = []
indexes = {}
for item in data_list:
item_data = [part.strip() for part in item.split("|")]
for idx, part in enumerate(item_data):
if re.match("[0-9]* W", part):
item_data[idx] = item_data[idx].replace(" W", "")
indexes['wattage'] = idx
elif re.match("[0-9]*\.?[0-9]* TH\/s", part):
item_data[idx] = item_data[idx].replace(" TH/s", "")
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]?)", part):
item_data[idx] = ipaddress.ip_address(item_data[idx])
indexes['ip'] = idx
new_list.append(item_data)
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:
if index.lower() not in indexes.keys():
return
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", "")

68
cfg_util/func/files.py Normal file
View File

@@ -0,0 +1,68 @@
import ipaddress
import os
import re
import time
import aiofiles
import toml
from cfg_util.func.ui import update_ui_with_data
from cfg_util.layout import window
from config.bos import bos_config_convert, general_config_convert_bos
async def import_iplist(file_location):
await update_ui_with_data("status", "Importing")
if not os.path.exists(file_location):
return
else:
ip_list = []
async with aiofiles.open(file_location, mode='r') as file:
async for line in file:
ips = [x.group() for x in re.finditer(
"^((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]?)", line)]
for ip in ips:
if ip not in ip_list:
ip_list.append(ipaddress.ip_address(ip))
ip_list.sort()
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("status", "")
async def export_iplist(file_location, ip_list_selected):
await update_ui_with_data("status", "Exporting")
if not os.path.exists(file_location):
return
else:
if ip_list_selected is not None and not ip_list_selected == []:
async with aiofiles.open(file_location, mode='w') as file:
for item in ip_list_selected:
await file.write(str(item) + "\n")
else:
async with aiofiles.open(file_location, mode='w') as file:
for item in window['ip_table'].Values:
await file.write(str(item[0]) + "\n")
await update_ui_with_data("status", "")
async def import_config_file(file_location):
await update_ui_with_data("status", "Importing")
if not os.path.exists(file_location):
return
else:
async with aiofiles.open(file_location, mode='r') as file:
config = await file.read()
await update_ui_with_data("config", await bos_config_convert(toml.loads(config)))
await update_ui_with_data("status", "")
async def export_config_file(file_location, config):
await update_ui_with_data("status", "Exporting")
config = toml.loads(config)
config['format']['generator'] = 'upstream_config_util'
config['format']['timestamp'] = int(time.time())
config = toml.dumps(config)
async with aiofiles.open(file_location, mode='w+') as file:
await file.write(await general_config_convert_bos(config))
await update_ui_with_data("status", "")

301
cfg_util/func/miners.py Normal file
View File

@@ -0,0 +1,301 @@
import asyncio
import ipaddress
import time
import warnings
from API import APIError, APIWarning
from cfg_util.func.parse_data import safe_parse_api_data
from cfg_util.func.ui import update_ui_with_data, update_prog_bar, set_progress_bar_len
from cfg_util.layout import window
from cfg_util.miner_factory import miner_factory
from config.bos import bos_config_convert
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
async def import_config(idx):
await update_ui_with_data("status", "Importing")
miner = await miner_factory.get_miner(ipaddress.ip_address(window["ip_table"].Values[idx[0]][0]))
await miner.get_config()
config = miner.config
await update_ui_with_data("config", str(config))
await update_ui_with_data("status", "")
async def scan_network(network):
await update_ui_with_data("status", "Scanning")
await update_ui_with_data("ip_count", "")
await update_ui_with_data("hr_total", "")
window["ip_table"].update([])
network_size = len(network)
miner_generator = network.scan_network_generator()
await set_progress_bar_len(2 * network_size)
progress_bar_len = 0
miners = []
async for miner in miner_generator:
if 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
asyncio.create_task(update_prog_bar(progress_bar_len))
progress_bar_len += network_size - len(miners)
asyncio.create_task(update_prog_bar(progress_bar_len))
get_miner_genenerator = miner_factory.get_miner_generator(miners)
all_miners = []
async for found_miner in get_miner_genenerator:
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
asyncio.create_task(update_prog_bar(progress_bar_len))
await update_ui_with_data("ip_count", str(len(all_miners)))
await update_ui_with_data("status", "")
async def miner_light(ips: list):
await asyncio.gather(*[flip_light(ip) for ip in ips])
async def flip_light(ip):
ip_list = window['ip_table'].Widget
miner = await miner_factory.get_miner(ip)
index = [item[0] for item in window["ip_table"].Values].index(ip)
index_tags = ip_list.item(index)['tags']
if "light" not in index_tags:
ip_list.item(index, tags=([*index_tags, "light"]))
window['ip_table'].update(row_colors=[(index, "white", "red")])
await miner.fault_light_on()
else:
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 send_config_generator(miners: list, config):
loop = asyncio.get_event_loop()
config_tasks = []
for miner in miners:
if len(config_tasks) >= CONFIG_THREADS:
configured = asyncio.as_completed(config_tasks)
config_tasks = []
for sent_config in configured:
yield await sent_config
config_tasks.append(loop.create_task(miner.send_config(config)))
configured = asyncio.as_completed(config_tasks)
for sent_config in configured:
yield await sent_config
async def send_config(ips: list, config):
await update_ui_with_data("status", "Configuring")
await set_progress_bar_len(2 * len(ips))
progress_bar_len = 0
get_miner_genenerator = miner_factory.get_miner_generator(ips)
all_miners = []
async for miner in get_miner_genenerator:
all_miners.append(miner)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
config_sender_generator = send_config_generator(all_miners, config)
async for _config_sender in config_sender_generator:
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
await update_ui_with_data("status", "")
async def get_data(ip_list: list):
await update_ui_with_data("status", "Getting Data")
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))
progress_bar_len = 0
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in ips])
ip_table_data = window["ip_table"].Values
ordered_all_ips = [item[0] for item in ip_table_data]
for all_data in data_gen:
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["model"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point["temp"],
data_point['user'], str(data_point['wattage']) + " W"
]
window["ip_table"].update(ip_table_data)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
hashrate_list = [float(item[3].replace(" TH/s", "")) for item in window["ip_table"].Values if not item[3] == '']
total_hr = round(sum(hashrate_list), 2)
window["hr_total"].update(f"{total_hr} TH/s")
await update_ui_with_data("status", "")
async def scan_and_get_data(network):
await update_ui_with_data("status", "Scanning")
network_size = len(network)
miner_generator = network.scan_network_generator()
await set_progress_bar_len(3 * network_size)
progress_bar_len = 0
miners = []
async for miner in miner_generator:
if 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
asyncio.create_task(update_prog_bar(progress_bar_len))
progress_bar_len += network_size - len(miners)
asyncio.create_task(update_prog_bar(progress_bar_len))
get_miner_genenerator = miner_factory.get_miner_generator(miners)
all_miners = []
async for found_miner in get_miner_genenerator:
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
asyncio.create_task(update_prog_bar(progress_bar_len))
await update_ui_with_data("ip_count", str(len(all_miners)))
data_gen = asyncio.as_completed([get_formatted_data(miner) for miner in miners])
ip_table_data = window["ip_table"].Values
ordered_all_ips = [item[0] for item in ip_table_data]
progress_bar_len += (network_size - len(miners))
asyncio.create_task(update_prog_bar(progress_bar_len))
await update_ui_with_data("status", "Getting Data")
for all_data in data_gen:
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["model"], data_point["host"], str(data_point['TH/s']) + " TH/s", data_point["temp"],
data_point['user'], str(data_point['wattage']) + " W"
]
window["ip_table"].update(ip_table_data)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
hashrate_list = [float(item[3].replace(" TH/s", "")) for item in window["ip_table"].Values if not item[3] == '']
total_hr = round(sum(hashrate_list), 2)
await update_ui_with_data("hr_total", f"{total_hr} TH/s")
await update_ui_with_data("status", "")
async def get_formatted_data(ip: ipaddress.ip_address):
miner = await miner_factory.get_miner(ip)
warnings.filterwarnings('ignore')
try:
miner_data = await miner.api.multicommand("summary", "devs", "temps", "tunerstatus", "pools", "stats")
except APIError:
return {'TH/s': "Unknown", 'IP': str(miner.ip), 'host': "Unknown", 'user': "Unknown", 'wattage': 0}
host = await miner.get_hostname()
model = await miner.get_model()
temps = 0
if "summary" in miner_data.keys():
if "Temperature" in miner_data['summary'][0]['SUMMARY'][0].keys():
if not round(miner_data['summary'][0]['SUMMARY'][0]["Temperature"]) == 0:
temps = miner_data['summary'][0]['SUMMARY'][0]["Temperature"]
if 'MHS 5s' in miner_data['summary'][0]['SUMMARY'][0].keys():
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():
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)
else:
th5s = 0
else:
th5s = 0
else:
th5s = 0
if "temps" in miner_data.keys() and not miner_data["temps"][0]['TEMPS'] == []:
if "Chip" in miner_data["temps"][0]['TEMPS'][0].keys():
for board in miner_data["temps"][0]['TEMPS']:
if board["Chip"] is not None and not board["Chip"] == 0.0:
temps = board["Chip"]
if "devs" in miner_data.keys() and not miner_data["devs"][0]['DEVS'] == []:
if "Chip Temp Avg" in miner_data["devs"][0]['DEVS'][0].keys():
for board in miner_data["devs"][0]['DEVS']:
if board['Chip Temp Avg'] is not None and not board['Chip Temp Avg'] == 0.0:
temps = board['Chip Temp Avg']
if "stats" in miner_data.keys() and not miner_data["stats"][0]['STATS'] == []:
for temp in ["temp2", "temp1", "temp3"]:
if temp in miner_data["stats"][0]['STATS'][1].keys():
if miner_data["stats"][0]['STATS'][1][temp] is not None and not miner_data["stats"][0]['STATS'][1][
temp] == 0.0:
temps = miner_data["stats"][0]['STATS'][1][temp]
if "pools" not in miner_data.keys():
user = "?"
elif not miner_data['pools'][0]['POOLS'] == []:
user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User')
else:
user = "Blank"
if "tunerstatus" in miner_data.keys():
wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit")
elif "Power" in miner_data["summary"][0]["SUMMARY"][0].keys():
wattage = await safe_parse_api_data(miner_data, "summary", 0, 'SUMMARY', 0, "Power")
else:
wattage = 0
return {'TH/s': th5s, 'IP': str(miner.ip), 'model': model,
'temp': round(temps), 'host': host, 'user': user,
'wattage': wattage}
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': [{
'name': 'group',
'quota': 1,
'pool': [{
'url': url_1,
'user': user,
'password': '123'
}, {
'url': url_2,
'user': user,
'password': '123'
}, {
'url': url_3,
'user': user,
'password': '123'
}]
}],
'format': {
'version': '1.2+',
'model': 'Antminer S9',
'generator': 'upstream_config_util',
'timestamp': int(time.time())
},
'temp_control': {
'target_temp': 80.0,
'hot_temp': 90.0,
'dangerous_temp': 120.0
},
'autotuning': {
'enabled': True,
'psu_power_limit': 900
}
}
window['config'].update(await bos_config_convert(config))

View File

@@ -1,6 +1,7 @@
from API import APIError
# noinspection PyPep8
async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = 0):
path = [*path]
if len(path) == idx+1:
@@ -18,6 +19,7 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int =
if path[idx] in data.keys():
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
# has to be == None, or else it fails on 0.0 hashrates
# noinspection PyPep8
if parsed_data == None:
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
return parsed_data
@@ -34,6 +36,7 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int =
if len(data) > path[idx]:
parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path)
# has to be == None, or else it fails on 0.0 hashrates
# noinspection PyPep8
if parsed_data == None:
raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}")
return parsed_data

72
cfg_util/func/ui.py Normal file
View File

@@ -0,0 +1,72 @@
import ipaddress
import re
from cfg_util.layout import window
import pyperclip
def copy_from_table(table):
selection = table.selection()
copy_values = []
for each in selection:
try:
value = table.item(each)["values"][0]
copy_values.append(str(value))
except:
pass
copy_string = "\n".join(copy_values)
pyperclip.copy(copy_string)
async def update_ui_with_data(key, message, append=False):
if append:
message = window[key].get_text() + message
window[key].update(message)
async def update_prog_bar(amount):
window["progress"].Update(amount)
percent_done = 100 * (amount / window['progress'].maxlen)
window["progress_percent"].Update(f"{round(percent_done, 2)} %")
if percent_done == 100:
window["progress_percent"].Update("")
async def set_progress_bar_len(amount):
window["progress"].Update(0, max=amount)
window["progress"].maxlen = amount
window["progress_percent"].Update("0.0 %")
async def sort_data(index: int or str):
await update_ui_with_data("status", "Sorting Data")
data_list = window['ip_table'].Values
# wattage
if re.match("[0-9]* W", str(data_list[0][index])):
new_list = sorted(data_list, key=lambda x: int(x[index].replace(" W", "")))
if data_list == new_list:
new_list = sorted(data_list, reverse=True, key=lambda x: int(x[index].replace(" W", "")))
# hashrate
elif re.match("[0-9]*\.?[0-9]* TH\/s", str(data_list[0][index])):
new_list = sorted(data_list, key=lambda x: float(x[index].replace(" TH/s", "")))
if data_list == new_list:
new_list = sorted(data_list, reverse=True, key=lambda x: float(x[index].replace(" TH/s", "")))
# ip addresses
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]?)",
str(data_list[0][index])):
new_list = sorted(data_list, key=lambda x: ipaddress.ip_address(x[index]))
if data_list == new_list:
new_list = sorted(data_list, reverse=True, key=lambda x: ipaddress.ip_address(x[index]))
# everything else, hostname, temp, and user
else:
new_list = sorted(data_list, key=lambda x: x[index])
if data_list == new_list:
new_list = sorted(data_list, reverse=True, key=lambda x: x[index])
await update_ui_with_data("ip_table", new_list)
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
miner_factory = MinerFactory()

View File

@@ -1,18 +1,34 @@
import asyncio
import sys
import PySimpleGUI as sg
from cfg_util.layout import window
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
from cfg_util.layout import window, generate_config_layout
from cfg_util.func.miners import scan_network, send_config, miner_light, get_data, generate_config, import_config, \
scan_and_get_data
from cfg_util.func.files import import_iplist, import_config_file, export_iplist, export_config_file
from cfg_util.func.ui import sort_data, copy_from_table
from network import MinerNetwork
import webbrowser
async def ui():
window.read(timeout=0)
table = window["ip_table"].Widget
table.bind("<Control-Key-c>", lambda x: copy_from_table(table))
while True:
event, value = window.read(timeout=10)
if event in (None, 'Close'):
if event in (None, 'Close', sg.WIN_CLOSED):
sys.exit()
if isinstance(event, tuple):
if len(window["ip_table"].Values) > 0:
if event[0] == 'ip_table':
if event[2][0] == -1:
await sort_data(event[2][1])
if event == 'open_in_web':
for row in value["ip_table"]:
webbrowser.open("http://" + window["ip_table"].Values[row][0])
if event == 'scan':
if len(value['miner_network'].split("/")) > 1:
network = value['miner_network'].split("/")
@@ -21,36 +37,51 @@ async def ui():
miner_network = MinerNetwork(value['miner_network'])
asyncio.create_task(scan_network(miner_network))
if event == 'select_all_ips':
if value['ip_list'] == window['ip_list'].Values:
window['ip_list'].set_value([])
if len(value["ip_table"]) == len(window["ip_table"].Values):
window["ip_table"].update(select_rows=())
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 2 > len(value['ip_list']) > 0:
asyncio.create_task(import_config(value['ip_list']))
if 2 > len(value['ip_table']) > 0:
asyncio.create_task(import_config(value['ip_table']))
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":
asyncio.create_task(import_iplist(value["file_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":
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":
asyncio.create_task(import_config_file(value['file_config']))
if event == "export_file_config":
asyncio.create_task(export_config_file(value['file_config'], value["config"]))
if event == "get_data":
asyncio.create_task(get_data(value['ip_list']))
if len(window["ip_table"].Values) == 0:
if len(value['miner_network'].split("/")) > 1:
network = value['miner_network'].split("/")
miner_network = MinerNetwork(ip_addr=network[0], mask=network[1])
else:
miner_network = MinerNetwork(value['miner_network'])
asyncio.create_task(scan_and_get_data(miner_network))
else:
asyncio.create_task(get_data([window["ip_table"].Values[item][0] for item in value["ip_table"]]))
if event == "generate_config":
asyncio.create_task(generate_config())
if event == "sort_data_ip":
asyncio.create_task(sort_data('ip'))
if event == "sort_data_hr":
asyncio.create_task(sort_data('hr'))
if event == "sort_data_user":
asyncio.create_task(sort_data(3))
if event == "sort_data_w":
asyncio.create_task(sort_data('wattage'))
await generate_config_ui()
if event == "__TIMEOUT__":
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 sys
import os
from cx_Freeze import setup, Executable
base = None
@@ -18,9 +17,15 @@ if sys.platform == "win32":
version = datetime.datetime.now()
version = version.strftime("%y.%m.%d")
print(version)
setup(name="UpstreamCFGUtil.exe",
version=version,
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/settings.toml"),
os.path.join(os.getcwd(), "static/CFG-Util-README.md")],
},
},
executables=[Executable("config_tool.py", base=base, icon="icon.ico", target_name="UpstreamCFGUtil.exe")]
)

View File

@@ -10,3 +10,5 @@ class BaseMiner:
def __init__(self, ip: str, api: BMMinerAPI | BOSMinerAPI | CGMinerAPI | BTMinerAPI | UnknownAPI) -> None:
self.ip = ipaddress.ip_address(ip)
self.api = api
self.api_type = None
self.model = None

View File

@@ -0,0 +1,11 @@
from miners.bmminer import BMMiner
class BMMinerS9(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.model = "S9"
self.api_type = "BMMiner"
def __repr__(self) -> str:
return f"BMMinerS9: {str(self.ip)}"

View File

@@ -1,118 +0,0 @@
from miners import BaseMiner
from API.bosminer import BOSMinerAPI
import asyncssh
import toml
from config.bos import bos_config_convert, general_config_convert_bos
class BOSMinerS9(BaseMiner):
def __init__(self, ip: str) -> None:
api = BOSMinerAPI(ip)
super().__init__(ip, api)
self.config = None
self.uname = 'root'
self.pwd = 'admin'
def __repr__(self) -> str:
return f"S9 - BOSminer: {str(self.ip)}"
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
server_host_key_algs=['ssh-rsa'])
# return created connection
return conn
async def send_ssh_command(self, cmd: str) -> None:
"""Sends SSH command to miner."""
# creates result variable
result = None
# runs the command on the miner
async with (await self._get_ssh_connection()) as conn:
# attempt to run command up to 3 times
for i in range(3):
try:
# save result of the command
result = await conn.run(cmd)
except Exception as e:
print(f"{cmd} error: {e}")
if i == 3:
return
continue
# let the user know the result of the command
if result is not None:
if result.stdout != "":
print(result.stdout)
if result.stderr != "":
print("ERROR: " + result.stderr)
elif result.stderr != "":
print("ERROR: " + result.stderr)
else:
print(cmd)
async def fault_light_on(self) -> None:
"""Sends command to turn on fault light on the miner."""
await self.send_ssh_command('miner fault_light on')
async def fault_light_off(self) -> None:
"""Sends command to turn off fault light on the miner."""
await self.send_ssh_command('miner fault_light off')
async def restart_backend(self) -> None:
"""Restart bosminer hashing process."""
await self.send_ssh_command('/etc/init.d/bosminer restart')
async def reboot(self) -> None:
"""Reboots power to the physical miner."""
await self.send_ssh_command('/sbin/reboot')
async def get_config(self) -> None:
async with (await self._get_ssh_connection()) as conn:
async with conn.start_sftp_client() as sftp:
async with sftp.open('/etc/bosminer.toml') as file:
toml_data = toml.loads(await file.read())
cfg = await bos_config_convert(toml_data)
self.config = cfg
async def get_hostname(self) -> str:
"""Attempts to get hostname from miner."""
try:
async with (await self._get_ssh_connection()) as conn:
data = await conn.run('cat /proc/sys/kernel/hostname')
return data.stdout.strip()
except Exception as e:
print(self.ip, e)
return "BOSMiner Unknown"
async def send_config(self, yaml_config) -> None:
"""Configures miner with yaml config."""
toml_conf = await general_config_convert_bos(yaml_config)
async with (await self._get_ssh_connection()) as conn:
async with conn.start_sftp_client() as sftp:
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
await file.write(toml_conf)
await conn.run("/etc/init.d/bosminer restart")
async def get_bad_boards(self) -> list:
"""Checks for and provides list of non working boards."""
devs = await self.api.devdetails()
bad = 0
chains = devs['DEVDETAILS']
for chain in chains:
if chain['Chips'] == 0:
bad += 1
if bad > 0:
return [str(self.ip), bad]
async def check_good_boards(self) -> str:
"""Checks for and provides list for working boards."""
devs = await self.api.devdetails()
bad = 0
chains = devs['DEVDETAILS']
for chain in chains:
if chain['Chips'] == 0:
bad += 1
if not bad > 0:
return str(self.ip)

View File

@@ -0,0 +1,11 @@
from miners.bosminer import BOSminer
class BOSMinerS9(BOSminer):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.model = "S9"
self.api_type = "BOSMiner"
def __repr__(self) -> str:
return f"BOSminerS9: {str(self.ip)}"

View File

@@ -0,0 +1,11 @@
from miners.cgminer import CGMiner
class CGMinerS9(CGMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.model = "S9"
self.api_type = "CGMiner"
def __repr__(self) -> str:
return f"CGMinerS9: {str(self.ip)}"

View File

@@ -0,0 +1,9 @@
from miners.bmminer import BMMiner
class BMMinerX17(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
def __repr__(self) -> str:
return f"CGMinerX17: {str(self.ip)}"

View File

@@ -1,118 +0,0 @@
from miners import BaseMiner
from API.bosminer import BOSMinerAPI
import asyncssh
import toml
from config.bos import bos_config_convert, general_config_convert_bos
class BOSminerX17(BaseMiner):
def __init__(self, ip: str) -> None:
api = BOSMinerAPI(ip)
super().__init__(ip, api)
self.config = None
self.uname = 'root'
self.pwd = 'admin'
def __repr__(self) -> str:
return f"X17 - BOSminer: {str(self.ip)}"
async def _get_ssh_connection(self) -> asyncssh.connect:
"""Create a new asyncssh connection"""
conn = await asyncssh.connect(str(self.ip), known_hosts=None, username=self.uname, password=self.pwd,
server_host_key_algs=['ssh-rsa'])
# return created connection
return conn
async def send_ssh_command(self, cmd: str) -> None:
"""Sends SSH command to miner."""
# creates result variable
result = None
# runs the command on the miner
async with (await self._get_ssh_connection()) as conn:
# attempt to run command up to 3 times
for i in range(3):
try:
# save result of the command
result = await conn.run(cmd)
except Exception as e:
print(f"{cmd} error: {e}")
if i == 3:
return
continue
# let the user know the result of the command
if result is not None:
if result.stdout != "":
print(result.stdout)
if result.stderr != "":
print("ERROR: " + result.stderr)
elif result.stderr != "":
print("ERROR: " + result.stderr)
else:
print(cmd)
async def fault_light_on(self) -> None:
"""Sends command to turn on fault light on the miner."""
await self.send_ssh_command('miner fault_light on')
async def fault_light_off(self) -> None:
"""Sends command to turn off fault light on the miner."""
await self.send_ssh_command('miner fault_light off')
async def restart_backend(self) -> None:
"""Restart bosminer hashing process."""
await self.send_ssh_command('/etc/init.d/bosminer restart')
async def reboot(self) -> None:
"""Reboots power to the physical miner."""
await self.send_ssh_command('/sbin/reboot')
async def get_config(self) -> None:
async with (await self._get_ssh_connection()) as conn:
async with conn.start_sftp_client() as sftp:
async with sftp.open('/etc/bosminer.toml') as file:
toml_data = toml.loads(await file.read())
cfg = await bos_config_convert(toml_data)
self.config = cfg
async def get_hostname(self) -> str:
"""Attempts to get hostname from miner."""
try:
async with (await self._get_ssh_connection()) as conn:
data = await conn.run('cat /proc/sys/kernel/hostname')
return data.stdout.strip()
except Exception as e:
print(self.ip, e)
return "BOSMiner Unknown"
async def send_config(self, yaml_config) -> None:
"""Configures miner with yaml config."""
toml_conf = await general_config_convert_bos(yaml_config)
async with (await self._get_ssh_connection()) as conn:
async with conn.start_sftp_client() as sftp:
async with sftp.open('/etc/bosminer.toml', 'w+') as file:
await file.write(toml_conf)
await conn.run("/etc/init.d/bosminer restart")
async def get_bad_boards(self) -> list:
"""Checks for and provides list of non working boards."""
devs = await self.api.devdetails()
bad = 0
chains = devs['DEVDETAILS']
for chain in chains:
if chain['Chips'] == 0:
bad += 1
if bad > 0:
return [str(self.ip), bad]
async def check_good_boards(self) -> str:
"""Checks for and provides list for working boards."""
devs = await self.api.devdetails()
bad = 0
chains = devs['DEVDETAILS']
for chain in chains:
if chain['Chips'] == 0:
bad += 1
if not bad > 0:
return str(self.ip)

View File

@@ -0,0 +1,10 @@
from miners.bosminer import BOSminer
class BOSMinerX17(BOSminer):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.api_type = "BOSMiner"
def __repr__(self) -> str:
return f"BOSminerX17: {str(self.ip)}"

View File

@@ -0,0 +1,10 @@
from miners.cgminer import CGMiner
class CGMinerX17(CGMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.api_type = "CGMiner"
def __repr__(self) -> str:
return f"CGMinerX17: {str(self.ip)}"

View File

@@ -0,0 +1,18 @@
from miners.bmminer import BMMiner
class BMMinerX19(BMMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
def __repr__(self) -> str:
return f"BMMinerX19: {str(self.ip)}"
async def get_model(self):
if self.model:
return self.model
version_data = await self.api.version()
if version_data:
self.model = version_data["VERSION"][0]["Type"].replace("Antminer ", "")
return self.model
return None

View File

@@ -0,0 +1,19 @@
from miners.cgminer import CGMiner
class CGMinerX19(CGMiner):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.api_type = "CGMiner"
def __repr__(self) -> str:
return f"CGMinerX19: {str(self.ip)}"
async def get_model(self):
if self.model:
return self.model
version_data = await self.api.version()
if version_data:
self.model = version_data["VERSION"][0]["Type"].replace("Antminer ", "")
return self.model
return None

View File

@@ -5,13 +5,23 @@ from miners import BaseMiner
class BMMiner(BaseMiner):
def __init__(self, ip: str) -> None:
api = BMMinerAPI(ip)
self.model = None
super().__init__(ip, api)
def __repr__(self) -> str:
return f"BMMiner: {str(self.ip)}"
async def get_model(self):
if self.model:
return self.model
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model
return None
async def get_hostname(self) -> str:
return "BMMiner Unknown"
return "?"
async def send_config(self, _):
return None # ignore for now

View File

@@ -9,6 +9,7 @@ class BOSminer(BaseMiner):
def __init__(self, ip: str) -> None:
api = BOSMinerAPI(ip)
super().__init__(ip, api)
self.model = None
self.config = None
self.uname = 'root'
self.pwd = 'admin'
@@ -86,6 +87,15 @@ class BOSminer(BaseMiner):
print(self.ip, e)
return "BOSMiner Unknown"
async def get_model(self):
if self.model:
return self.model
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model
return None
async def send_config(self, yaml_config) -> None:
"""Configures miner with yaml config."""
toml_conf = await general_config_convert_bos(yaml_config)

View File

@@ -1,17 +1,33 @@
from API.btminer import BTMinerAPI
from miners import BaseMiner
from API import APIError
class BTMiner(BaseMiner):
def __init__(self, ip: str) -> None:
api = BTMinerAPI(ip)
self.model = None
super().__init__(ip, api)
def __repr__(self) -> str:
return f"BTMiner: {str(self.ip)}"
async def get_model(self):
if self.model:
return self.model
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].split("V")[0]
return self.model
return None
async def get_hostname(self) -> str:
return "BTMiner Unknown"
try:
host_data = await self.api.get_miner_info()
if host_data:
return host_data["Msg"]["hostname"]
except APIError:
return "?"
async def send_config(self, _):
return None # ignore for now

View File

@@ -7,6 +7,7 @@ class CGMiner(BaseMiner):
def __init__(self, ip: str) -> None:
api = CGMinerAPI(ip)
super().__init__(ip, api)
self.model = None
self.config = None
self.uname = 'root'
self.pwd = 'admin'
@@ -14,6 +15,15 @@ class CGMiner(BaseMiner):
def __repr__(self) -> str:
return f"CGMiner: {str(self.ip)}"
async def get_model(self):
if self.model:
return self.model
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model
return None
async def get_hostname(self) -> str:
try:
async with (await self._get_ssh_connection()) as conn:
@@ -21,9 +31,9 @@ class CGMiner(BaseMiner):
data = await conn.run('cat /proc/sys/kernel/hostname')
return data.stdout.strip()
else:
return "CGMiner Unknown"
return "?"
except Exception:
return "CGMiner Unknown"
return "?"
async def send_config(self, _):
return None # ignore for now
@@ -67,10 +77,15 @@ class CGMiner(BaseMiner):
@staticmethod
def _result_handler(result: asyncssh.process.SSHCompletedProcess) -> None:
if result is not None:
# noinspection PyUnresolvedReferences
if len(result.stdout) > 0:
# noinspection PyUnresolvedReferences
print("ssh stdout: \n" + result.stdout)
# noinspection PyUnresolvedReferences
if len(result.stderr) > 0:
# noinspection PyUnresolvedReferences
print("ssh stderr: \n" + result.stderrr)
# noinspection PyUnresolvedReferences
if len(result.stdout) <= 0 and len(result.stderr) <= 0:
print("ssh stdout stderr empty")
# if result.stdout != "":

View File

@@ -1,7 +1,23 @@
from miners.bosminer import BOSminer
from miners.antminer.S9.bosminer import BOSMinerS9
from miners.antminer.S9.bmminer import BMMinerS9
from miners.antminer.S9.cgminer import CGMinerS9
from miners.antminer.X17.bosminer import BOSMinerX17
from miners.antminer.X17.bmminer import BMMinerX17
from miners.antminer.X17.cgminer import CGMinerX17
from miners.antminer.X19.bmminer import BMMinerX19
from miners.antminer.X19.cgminer import CGMinerX19
from miners.whatsminer.M20 import BTMinerM20
from miners.whatsminer.M21 import BTMinerM21
from miners.whatsminer.M30 import BTMinerM30
from miners.whatsminer.M31 import BTMinerM31
from miners.whatsminer.M32 import BTMinerM32
from miners.bmminer import BMMiner
from miners.cgminer import CGMiner
from miners.btminer import BTMiner
from miners.unknown import UnknownMiner
from API import APIError
import asyncio
@@ -16,6 +32,14 @@ class MinerFactory:
self.miners = {}
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()
scan_tasks = []
for miner in ips:
@@ -24,37 +48,53 @@ class MinerFactory:
for miner in scanned:
yield await miner
async def get_miner(self, ip: ipaddress.ip_address) -> BOSminer or CGMiner or BMMiner or UnknownMiner:
async def get_miner(self, ip: ipaddress.ip_address):
"""Decide a miner type using the IP address of the miner."""
# check if the miner already exists in cache
if ip in self.miners:
return self.miners[ip]
# get the version data
version = None
miner = UnknownMiner(str(ip))
api = None
for i in range(GET_VERSION_RETRIES):
version_data = await self._get_version_data(ip)
if version_data:
# if we got version data, get a list of the keys so we can check type of miner
version = list(version_data['VERSION'][0].keys())
api = await self._get_api_type(ip)
if api:
break
if version:
# check version against different return miner types
if "BOSminer" in version or "BOSminer+" in version:
miner = BOSminer(str(ip))
elif "CGMiner" in version:
miner = CGMiner(str(ip))
elif "BMMiner" in version:
miner = BMMiner(str(ip))
elif "BTMiner" in version:
miner = BTMiner(str(ip))
else:
print(f"Bad API response: {version}")
miner = UnknownMiner(str(ip))
else:
# if we don't get version, miner type is unknown
print(f"No API response: {str(ip)}")
miner = UnknownMiner(str(ip))
# save the miner in cache
model = None
for i in range(GET_VERSION_RETRIES):
model = await self._get_miner_model(ip)
if model:
break
if model:
if "Antminer" in model:
if model == "Antminer S9":
if "BOSMiner" in api:
miner = BOSMinerS9(str(ip))
elif "CGMiner" in api:
miner = CGMinerS9(str(ip))
elif "BMMiner" in api:
miner = BMMinerS9(str(ip))
elif "17" in model:
if "BOSMiner" in api:
miner = BOSMinerX17(str(ip))
elif "CGMiner" in api:
miner = CGMinerX17(str(ip))
elif "BMMiner" in api:
miner = BMMinerX17(str(ip))
elif "19" in model:
if "CGMiner" in api:
miner = CGMinerX19(str(ip))
elif "BMMiner" in api:
miner = BMMinerX19(str(ip))
elif "M20" in model:
miner = BTMinerM20(str(ip))
elif "M21" in model:
miner = BTMinerM21(str(ip))
elif "M30" in model:
miner = BTMinerM30(str(ip))
elif "M31" in model:
miner = BTMinerM31(str(ip))
elif "M32" in model:
miner = BTMinerM32(str(ip))
self.miners[ip] = miner
return miner
@@ -62,101 +102,107 @@ class MinerFactory:
"""Clear the miner factory cache."""
self.miners = {}
@staticmethod
async def _get_version_data(ip: ipaddress.ip_address) -> dict or None:
"""Get data on the version of the miner to return the right miner."""
for i in range(3):
async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> dict or None:
model = None
try:
# open a connection to the miner
fut = asyncio.open_connection(str(ip), 4028)
# get reader and writer streams
data = await self._send_api_command(str(ip), "devdetails")
if data.get("STATUS"):
if data["STATUS"][0].get("STATUS") not in ["I", "S"]:
try:
reader, writer = await asyncio.wait_for(fut, timeout=7)
except asyncio.exceptions.TimeoutError:
return None
# create the command
cmd = {"command": "version"}
# send the command
writer.write(json.dumps(cmd).encode('utf-8'))
await writer.drain()
# instantiate data
data = b""
# loop to receive all the data
while True:
d = await reader.read(4096)
if not d:
break
data += d
if data.endswith(b"\x00"):
data = json.loads(data.decode('utf-8')[:-1])
data = await self._send_api_command(str(ip), "version")
model = data["VERSION"][0]["Type"]
except:
print(f"Get Model Exception: {ip}")
else:
# some stupid whatsminers need a different command
fut = asyncio.open_connection(str(ip), 4028)
# get reader and writer streams
try:
reader, writer = await asyncio.wait_for(fut, timeout=7)
except asyncio.exceptions.TimeoutError:
return None
# create the command
cmd = {"command": "get_version"}
# send the command
writer.write(json.dumps(cmd).encode('utf-8'))
await writer.drain()
# instantiate data
data = b""
# loop to receive all the data
while True:
d = await reader.read(4096)
if not d:
break
data += d
data = data.decode('utf-8').replace("\n", "")
data = json.loads(data)
# close the connection
writer.close()
await writer.wait_closed()
# check if the data returned is correct or an error
# if status isn't a key, it is a multicommand
if "STATUS" not in data.keys():
for key in data.keys():
# make sure not to try to turn id into a dict
if not key == "id":
# make sure they succeeded
if data[key][0]["STATUS"][0]["STATUS"] not in ["S", "I"]:
# this is an error
raise APIError(data["STATUS"][0]["Msg"])
else:
# check for stupid whatsminer formatting
if not isinstance(data["STATUS"], list):
if data["STATUS"] not in ("S", "I"):
raise APIError(data["Msg"])
else:
if "whatsminer" in data["Description"]:
return {"VERSION": [{"BTMiner": data["Description"]}]}
# make sure the command succeeded
elif data["STATUS"][0]["STATUS"] not in ("S", "I"):
# this is an error
raise APIError(data["STATUS"][0]["Msg"])
# return the data
return data
model = data["DEVDETAILS"][0]["Model"]
if model:
return model
except OSError as e:
if e.winerror == 121:
return None
else:
print(ip, e)
return None
async def _send_api_command(self, ip: ipaddress.ip_address or str, command: str):
try:
# get reader and writer streams
reader, writer = await asyncio.open_connection(str(ip), 4028)
# handle OSError 121
except OSError as e:
if e.winerror == "121":
print("Semaphore Timeout has Expired.")
return {}
# create the command
cmd = {"command": command}
# send the command
writer.write(json.dumps(cmd).encode('utf-8'))
await writer.drain()
# instantiate data
data = b""
# loop to receive all the data
try:
while True:
d = await reader.read(4096)
if not d:
break
data += d
except Exception as e:
print(e)
try:
# some json from the API returns with a null byte (\x00) on the end
if data.endswith(b"\x00"):
# handle the null byte
str_data = data.decode('utf-8')[:-1]
else:
# no null byte
str_data = data.decode('utf-8')
# fix an error with a btminer return having an extra comma that breaks json.loads()
str_data = str_data.replace(",}", "}")
# fix an error with a btminer return having a newline that breaks json.loads()
str_data = str_data.replace("\n", "")
# fix an error with a bmminer return not having a specific comma that breaks json.loads()
str_data = str_data.replace("}{", "},{")
# parse the json
parsed_data = json.loads(str_data)
# handle bad json
except json.decoder.JSONDecodeError as e:
print(e)
raise APIError(f"Decode Error: {data}")
data = parsed_data
# close the connection
writer.close()
await writer.wait_closed()
return data
async def _get_api_type(self, ip: ipaddress.ip_address or str) -> dict or None:
"""Get data on the version of the miner to return the right miner."""
api = None
try:
data = await self._send_api_command(str(ip), "version")
if data.get("STATUS") and not data.get("STATUS") == "E":
if data["STATUS"][0].get("STATUS") in ["I", "S"]:
if "BMMiner" in data["VERSION"][0].keys():
api = "BMMiner"
elif "CGMiner" in data["VERSION"][0].keys():
api = "CGMiner"
elif "BOSminer" in data["VERSION"][0].keys() or "BOSminer+" in data["VERSION"][0].keys():
api = "BOSMiner"
elif data.get("Description") and "whatsminer" in data.get("Description"):
api = "BTMiner"
if api:
return api
except OSError as e:
if e.winerror == 121:
return None
else:
print(ip, e)
# except json.decoder.JSONDecodeError:
# print("Decode Error @ " + str(ip) + str(data))
# except Exception as e:
# print(ip, e)
return None

View File

@@ -10,6 +10,9 @@ class UnknownMiner(BaseMiner):
def __repr__(self) -> str:
return f"Unknown: {str(self.ip)}"
def get_model(self):
return "Unknown"
async def send_config(self, _):
return None

View File

@@ -1,26 +1,9 @@
from API.btminer import BTMinerAPI
from miners import BaseMiner
from miners.btminer import BTMiner
class BTMinerM20(BaseMiner):
class BTMinerM20(BTMiner):
def __init__(self, ip: str) -> None:
api = BTMinerAPI(ip)
super().__init__(ip, api)
super().__init__(ip)
def __repr__(self) -> str:
return f"M20 - BTMiner: {str(self.ip)}"
async def get_hostname(self) -> str:
return "BTMiner Unknown"
async def send_config(self):
return None # ignore for now
async def restart_backend(self) -> None:
return None
async def reboot(self) -> None:
return None
async def get_config(self) -> None:
return None

View File

@@ -1,26 +1,9 @@
from API.btminer import BTMinerAPI
from miners import BaseMiner
from miners.btminer import BTMiner
class BTMinerM21(BaseMiner):
class BTMinerM21(BTMiner):
def __init__(self, ip: str) -> None:
api = BTMinerAPI(ip)
super().__init__(ip, api)
super().__init__(ip)
def __repr__(self) -> str:
return f"M21 - BTMiner: {str(self.ip)}"
async def get_hostname(self) -> str:
return "BTMiner Unknown"
async def send_config(self):
return None # ignore for now
async def restart_backend(self) -> None:
return None
async def reboot(self) -> None:
return None
async def get_config(self) -> None:
return None

View File

@@ -1,26 +1,9 @@
from API.btminer import BTMinerAPI
from miners import BaseMiner
from miners.btminer import BTMiner
class BTMinerM30(BaseMiner):
class BTMinerM30(BTMiner):
def __init__(self, ip: str) -> None:
api = BTMinerAPI(ip)
super().__init__(ip, api)
super().__init__(ip)
def __repr__(self) -> str:
return f"M30- BTMiner: {str(self.ip)}"
async def get_hostname(self) -> str:
return "BTMiner Unknown"
async def send_config(self):
return None # ignore for now
async def restart_backend(self) -> None:
return None
async def reboot(self) -> None:
return None
async def get_config(self) -> None:
return None

View File

@@ -1,26 +1,9 @@
from API.btminer import BTMinerAPI
from miners import BaseMiner
from miners.btminer import BTMiner
class BTMinerM31(BaseMiner):
class BTMinerM31(BTMiner):
def __init__(self, ip: str) -> None:
api = BTMinerAPI(ip)
super().__init__(ip, api)
super().__init__(ip)
def __repr__(self) -> str:
return f"M31 - BTMiner: {str(self.ip)}"
async def get_hostname(self) -> str:
return "BTMiner Unknown"
async def send_config(self):
return None # ignore for now
async def restart_backend(self) -> None:
return None
async def reboot(self) -> None:
return None
async def get_config(self) -> None:
return None

View File

@@ -1,26 +1,9 @@
from API.btminer import BTMinerAPI
from miners import BaseMiner
from miners.btminer import BTMiner
class BTMinerM32(BaseMiner):
class BTMinerM32(BTMiner):
def __init__(self, ip: str) -> None:
api = BTMinerAPI(ip)
super().__init__(ip, api)
super().__init__(ip)
def __repr__(self) -> str:
return f"M32 - BTMiner: {str(self.ip)}"
async def get_hostname(self) -> str:
return "BTMiner Unknown"
async def send_config(self):
return None # ignore for now
async def restart_backend(self) -> None:
return None
async def reboot(self) -> None:
return None
async def get_config(self) -> None:
return None

View File

@@ -31,7 +31,7 @@ class MinerNetwork:
return ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False)
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()
print(f"Scanning {local_network} for miners...")
scan_tasks = []
@@ -55,6 +55,11 @@ class MinerNetwork:
return miners
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()
local_network = self.get_network()
scan_tasks = []

Binary file not shown.

64
static/CFG-Util-README.md Normal file
View File

@@ -0,0 +1,64 @@
# 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 separated 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
### Buttons:
* ALL: Selects all miners in the table, or deselects all if they are already all selected.
* GET DATA: Gets data for the currently selected miners, or all miners if none are selected.
* Additionally, if no miners have been scanned, this will also run a scan then get data on those miners.
* OPEN IN WEB: Opens all currently selected miners web interfaces in your default browser.
### Table:
* Click any header in the table to sort that row.
* You can copy (CTRL + C) a list of IP's directly from the rows selected in the table.
* #### IP:
* Contains all the IP's scanned
* #### Model:
* The model of the miners scanned.
* #### Hostname:
* The hostname of the miners scanned.
* ? will be displayed if the tool is unable to get it.
* #### Hashrate:
* The hashrate of the miners scanned.
* #### Temperature:
* The average board temperature of the miners scanned.
* #### Current User:
* The current first pool user of the miners scanned.
* #### Wattage
* The current wattage of the miners scanned.
* 0 W will be displayed if it is unknown.
### 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