Compare commits

..

30 Commits

Author SHA1 Message Date
UpstreamData
2734caa9da added (BOS) tag to braiins miners scanned 2022-01-07 15:33:40 -07:00
UpstreamData
d9ecdfc9d7 fixe a bug with older versions of braiins sometimes being buggy with versioning 2022-01-07 15:25:11 -07:00
UpstreamData
fa88bea376 switched over to GH/s av and MH/s av for hashrate 2022-01-06 14:44:26 -08:00
UpstreamData
25803b856d fixed an issue with getting model causing an error because of whatsminers 2022-01-07 13:45:23 -07:00
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
41 changed files with 1001 additions and 907 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,15 +56,14 @@ 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:
# 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)
user_commands = [*commands]
allowed_commands = self.get_commands()
# make sure we can actually run the command, otherwise it will fail
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)
@@ -109,14 +123,15 @@ class BaseMinerAPI:
writer.close()
await writer.wait_closed()
# validate the command suceeded
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
@@ -125,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

@@ -104,9 +104,6 @@ class BOSMinerAPI(BaseMinerAPI):
Returns a dict containing stats for all device/pool with more than 1 getwork,
ignoring zombie devices.
Parameters:
old (optional): include zombie devices that became zombies less than 'old' seconds ago.
"""
return await self.send_command("estats")

View File

@@ -1,6 +1,6 @@
from API import BaseMinerAPI, APIError
from passlib.handlers import md5_crypt
from passlib.handlers.md5_crypt import md5_crypt
import asyncio
import re
import json
@@ -132,8 +132,9 @@ class BTMinerAPI(BaseMinerAPI):
print(e)
# if it fails to validate, it is likely an error
if not self.validate_command_output(data):
raise APIError(data["Msg"])
validation = self.validate_command_output(data)
if not validation[0]:
raise APIError(validation[1])
# return the parsed json as a dict
return data
@@ -513,6 +514,14 @@ class BTMinerAPI(BaseMinerAPI):
"""
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")
@@ -525,24 +534,10 @@ class BTMinerAPI(BaseMinerAPI):
"""
return await self.send_command("status")
async def get_miner_info(self, info: str | list):
async def get_miner_info(self):
"""
API 'get_miner_info' command.
Returns a dict containing requested information.
Parameters:
info: the info that you want to get.
"ip",
"proto",
"netmask",
"gateway",
"dns",
"hostname",
"mac",
"ledstat".
Returns a dict containing general miner info.
"""
if isinstance(info, str):
return await self.send_command("get_miner_info", parameters=info)
else:
return await self.send_command("get_miner_info", parameters=f"{','.join([str(item) for item in info])}")
return await self.send_command("get_miner_info")

View File

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

View File

@@ -46,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
@@ -80,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
@@ -106,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
@@ -128,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,324 +0,0 @@
import asyncio
import ipaddress
import os
import re
import time
import aiofiles
import toml
from API import APIError
from cfg_util.func.data import safe_parse_api_data
from cfg_util.layout import window
from cfg_util.miner_factory import miner_factory
from config.bos import bos_config_convert, general_config_convert_bos
from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS
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 scan_network(network):
await update_ui_with_data("status", "Scanning")
await update_ui_with_data("hr_total", "")
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 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 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 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]
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["host"], str(data_point['TH/s']) + " TH/s", data_point['user'], str(data_point['wattage']) + " W"
]
window["ip_table"].update(ip_table_data)
progress_bar_len += 1
asyncio.create_task(update_prog_bar(progress_bar_len))
hashrate_list = [float(item[2].replace(" TH/s", "")) for item in window["ip_table"].Values]
total_hr = round(sum(hashrate_list), 2)
window["hr_total"].update(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)
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(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))
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", data_list[0][index]):
new_list = sorted(data_list, key=lambda x: int(x[index].replace(" W", "")))
# hashrate
elif re.match("[0-9]*\.?[0-9]* TH\/s", data_list[0][index]):
new_list = sorted(data_list, 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]?)",
data_list[0][index]):
new_list = sorted(data_list, key=lambda x: ipaddress.ip_address(x[index]))
# everything else, hostname and user
else:
new_list = sorted(data_list, key=lambda x: x[index])
await update_ui_with_data("ip_table", new_list)
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
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", "")) if not item[3] == '' else 0 for item in window["ip_table"].Values]
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 av' in miner_data['summary'][0]['SUMMARY'][0].keys():
th5s = round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS av') / 1000000, 2)
elif 'GHS av' in miner_data['summary'][0]['SUMMARY'][0].keys():
if not miner_data['summary'][0]['SUMMARY'][0]['GHS av'] == "":
th5s = round(float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS av')) / 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

@@ -3,17 +3,32 @@ import sys
import PySimpleGUI as sg
from cfg_util.layout import window, generate_config_layout
from cfg_util.func import scan_network, sort_data, send_config, miner_light, get_data, export_config_file, \
generate_config, import_config, import_iplist, import_config_file, export_iplist
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', 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("/")
@@ -42,17 +57,17 @@ async def ui():
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([window["ip_table"].Values[item][0] for item in value["ip_table"]]))
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":
await generate_config_ui()
if event == "sort_data_ip":
asyncio.create_task(sort_data(0)) # ip index in table
if event == "sort_data_hr":
asyncio.create_task(sort_data(2)) # HR index in table
if event == "sort_data_user":
asyncio.create_task(sort_data(3)) # user index in table
if event == "sort_data_w":
asyncio.create_task(sort_data(4)) # wattage index in table
if event == "__TIMEOUT__":
await asyncio.sleep(0)

View File

@@ -23,8 +23,8 @@ 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}\\",
"include_files": [os.path.join(os.getcwd(), "settings.toml"),
os.path.join(os.getcwd(), "CFG-Util-README.md")],
"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 + " (BOS)"
version_data = await self.api.devdetails()
if version_data:
self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "")
return self.model + " (BOS)"
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
@@ -32,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 "Antminer S9" in model:
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
@@ -70,101 +102,114 @@ 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):
try:
# open a connection to the miner
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": "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])
async def _get_miner_model(self, ip: ipaddress.ip_address or str) -> dict or None:
model = None
try:
data = await self._send_api_command(str(ip), "devdetails")
if data.get("STATUS"):
if not isinstance(data["STATUS"], str):
if data["STATUS"][0].get("STATUS") not in ["I", "S"]:
try:
data = await self._send_api_command(str(ip), "version")
model = data["VERSION"][0]["Type"]
except:
print(f"Get Model Exception: {ip}")
else:
model = data["DEVDETAILS"][0]["Model"]
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
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)
data = await self._send_api_command(str(ip), "version")
model = data["VERSION"][0]["Type"]
except:
print(f"Get Model Exception: {ip}")
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 any("BMMiner" in string for string in data["VERSION"][0].keys()):
api = "BMMiner"
elif any("CGMiner" in string for string in data["VERSION"][0].keys()):
api = "CGMiner"
elif any("BOSminer" in string for string 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)
return None

View File

@@ -10,6 +10,9 @@ class UnknownMiner(BaseMiner):
def __repr__(self) -> str:
return f"Unknown: {str(self.ip)}"
async 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
return f"M30- BTMiner: {str(self.ip)}"

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

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