From 7a098b1c7ee7063a3c599a57b6dd32175cebbb64 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Fri, 25 Mar 2022 15:29:30 -0600 Subject: [PATCH 01/25] added install file to do the basic install --- miners/__init__.py | 4 + network/__init__.py | 46 ++++---- tools/web_testbench/__init__.py | 191 ++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 tools/web_testbench/__init__.py diff --git a/miners/__init__.py b/miners/__init__.py index 014d4ead..f311faa3 100644 --- a/miners/__init__.py +++ b/miners/__init__.py @@ -44,6 +44,10 @@ class BaseMiner: logging.warning(f"{self} raised an exception: {e}") raise e + async def send_file(self, src, dest): + conn = self._get_ssh_connection() + await asyncssh.scp((conn, src), dest) + async def get_board_info(self): return None diff --git a/network/__init__.py b/network/__init__.py index a7ceca26..12be1b77 100644 --- a/network/__init__.py +++ b/network/__init__.py @@ -135,25 +135,29 @@ class MinerNetwork: @staticmethod async def ping_miner(ip: ipaddress.ip_address) -> None or ipaddress.ip_address: - for i in range(PING_RETRIES): - connection_fut = asyncio.open_connection(str(ip), 4028) - try: - # get the read and write streams from the connection - reader, writer = await asyncio.wait_for(connection_fut, timeout=PING_TIMEOUT) - # immediately close connection, we know connection happened - writer.close() - # make sure the writer is closed - await writer.wait_closed() - # ping was successful - return ip - except asyncio.exceptions.TimeoutError: - # ping failed if we time out - continue - except ConnectionRefusedError: - # handle for other connection errors - print(f"{str(ip)}: Connection Refused.") - # ping failed, likely with an exception - except Exception as e: - print(e) + return await ping_miner(ip) + + +async def ping_miner(ip: ipaddress.ip_address, port=4028) -> None or ipaddress.ip_address: + for i in range(PING_RETRIES): + connection_fut = asyncio.open_connection(str(ip), port) + try: + # get the read and write streams from the connection + reader, writer = await asyncio.wait_for(connection_fut, timeout=PING_TIMEOUT) + # immediately close connection, we know connection happened + writer.close() + # make sure the writer is closed + await writer.wait_closed() + # ping was successful + return ip + except asyncio.exceptions.TimeoutError: + # ping failed if we time out continue - return + except ConnectionRefusedError: + # handle for other connection errors + print(f"{str(ip)}: Connection Refused.") + # ping failed, likely with an exception + except Exception as e: + print(e) + continue + return diff --git a/tools/web_testbench/__init__.py b/tools/web_testbench/__init__.py new file mode 100644 index 00000000..d7b5998a --- /dev/null +++ b/tools/web_testbench/__init__.py @@ -0,0 +1,191 @@ +from ipaddress import ip_address +import asyncio +import os + +from network import ping_miner +from network.net_range import MinerNetworkRange +from miners.miner_factory import MinerFactory +from miners.antminer.S9.bosminer import BOSMinerS9 + +miner_network = MinerNetworkRange("192.168.1.1-192.168.1.38") + +REFERRAL_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "referral.ipk") +UPDATE_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "update.tar") +CONFIG_FILE = os.path.join(os.path.dirname(__file__), "files", "config.toml") + + +# static states +( + START, + UNLOCK, + INSTALL, + UPDATE, + REFERRAL, + DONE +) = range(6) + + +class testbenchMiner(): + def __init__(self, host: ip_address): + self.host = host + self.state = START + + async def add_to_output(self, message): + # send a message to web server + return + + async def remove_from_cache(self): + if self.host in MinerFactory().miners.keys(): + MinerFactory().miners.remove(self.host) + + async def wait_for_disconnect(self): + while await ping_miner(self.host): + await asyncio.sleep(1) + + async def install_start(self): + if not await ping_miner(self.host): + return + await self.remove_from_cache() + miner = MinerFactory().get_miner(self.host) + if isinstance(miner, BOSMinerS9): + self.state = UPDATE + return + if await ping_miner(self.host, 22): + self.state = INSTALL + return + self.state = UNLOCK + + async def install_unlock(self): + if await self.ssh_unlock(): + self.state = INSTALL + return + self.state = START + await self.wait_for_disconnect() + + async def ssh_unlock(self): + proc = await asyncio.create_subprocess_shell( + f'{os.path.join(os.path.dirname(__file__), "files", "asicseer_installer.exe")} -p -f {str(self.host)} root', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + if str(stdout).find("webUI") != -1: + return False + return True + + async def do_install(self): + proc = await asyncio.create_subprocess_shell( + f'{os.path.join(os.path.dirname(__file__), "files", "bos-toolbox", "bos-toolbox.bat")} install {str(self.host)} --no-keep-pools --psu-power-limit 900 --no-nand-backup --feeds-url file:./feeds/', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + # get stdout of the install + while True: + stdout = await proc.stderr.readuntil(b'\r') + if stdout == b'': + break + await proc.wait() + while not await ping_miner(self.host): + await asyncio.sleep(3) + await asyncio.sleep(5) + self.state = REFERRAL + + async def install_update(self): + await self.remove_from_cache() + miner = await MinerFactory().get_miner(self.host) + try: + await miner.send_file(UPDATE_FILE_S9, "/tmp/firmware.tar") + await miner.send_ssh_command("sysupgrade /tmp/firmware.tar") + except: + self.state = START + return + self.state = REFERRAL + + async def install_referral(self): + miner = await MinerFactory().get_miner(self.host) + if os.path.exists(REFERRAL_FILE_S9): + try: + await miner.send_file(REFERRAL_FILE_S9, '/tmp/referral.ipk') + await miner.send_file(CONFIG_FILE, '/etc/bosminer.toml') + + await miner.send_ssh_command('opkg install /tmp/referral.ipk && /etc/init.d/bosminer restart') + except: + self.state = START + return + else: + self.state = START + return + self.state = DONE + + async def get_web_data(self): + miner = await MinerFactory().get_miner(self.host) + + if not isinstance(miner, BOSMinerS9): + self.state = START + return + try: + all_data = await miner.api.multicommand("devs", "temps", "fans") + + devs_raw = all_data['devs'][0] + temps_raw = all_data['temps'][0] + fans_raw = all_data['fans'][0] + + # parse temperature data + temps_data = {} + for board in range(len(temps_raw['TEMPS'])): + temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"] = {} + temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"]["Board"] = temps_raw['TEMPS'][board]['Board'] + temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"]["Chip"] = temps_raw['TEMPS'][board]['Chip'] + + # parse individual board and chip temperature data + for board in temps_data.keys(): + if "Board" not in temps_data[board].keys(): + temps_data[board]["Board"] = 0 + if "Chip" not in temps_data[board].keys(): + temps_data[board]["Chip"] = 0 + + # parse hashrate data + hr_data = {} + for board in range(len(devs_raw['DEVS'])): + hr_data[f"board_{devs_raw['DEVS'][board]['ID']}"] = {} + hr_data[f"board_{devs_raw['DEVS'][board]['ID']}"]["HR"] = round( + devs_raw['DEVS'][board]['MHS 5s'] / 1000000, + 2) + + # parse fan data + fans_data = {} + for fan in range(len(fans_raw['FANS'])): + fans_data[f"fan_{fans_raw['FANS'][fan]['ID']}"] = {} + fans_data[f"fan_{fans_raw['FANS'][fan]['ID']}"]['RPM'] = fans_raw['FANS'][fan]['RPM'] + + # set the miner data + miner_data = { + 'IP': self.host, + "Light": "show", + 'Fans': fans_data, + 'HR': hr_data, + 'Temps': temps_data + } + + # return stats + return miner_data + except: + return + + async def install_done(self): + while await ping_miner(self.host) and self.state == DONE: + print(await self.get_web_data()) + await asyncio.sleep(1) + + async def install_loop(self): + while True: + if self.state == START: + await self.install_start() + if self.state == UNLOCK: + await self.install_unlock() + if self.state == INSTALL: + await self.do_install() + if self.state == UPDATE: + await self.install_update() + if self.state == REFERRAL: + await self.install_referral() + if self.state == DONE: + await self.install_done() From 22f78ac405d26ed62f37900b26a55a41bb1070d0 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Fri, 25 Mar 2022 16:02:50 -0600 Subject: [PATCH 02/25] add base files for web interface --- tools/web_monitor/dashboard/__init__.py | 1 - tools/web_testbench/app.py | 55 ++++ tools/web_testbench/public/create_layout.js | 261 ++++++++++++++++++ tools/web_testbench/public/events.js | 7 + tools/web_testbench/public/generate_graphs.js | 135 +++++++++ tools/web_testbench/public/graph_options.js | 59 ++++ tools/web_testbench/templates/index.html | 23 ++ 7 files changed, 540 insertions(+), 1 deletion(-) create mode 100644 tools/web_testbench/app.py create mode 100644 tools/web_testbench/public/create_layout.js create mode 100644 tools/web_testbench/public/events.js create mode 100644 tools/web_testbench/public/generate_graphs.js create mode 100644 tools/web_testbench/public/graph_options.js create mode 100644 tools/web_testbench/templates/index.html diff --git a/tools/web_monitor/dashboard/__init__.py b/tools/web_monitor/dashboard/__init__.py index 4944e4d9..d8be703f 100644 --- a/tools/web_monitor/dashboard/__init__.py +++ b/tools/web_monitor/dashboard/__init__.py @@ -17,7 +17,6 @@ def index(request: Request): @router.get("/dashboard") def dashboard(request: Request): - print() return templates.TemplateResponse("index.html", { "request": request, "cur_miners": get_current_miner_list() diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py new file mode 100644 index 00000000..a09a22b3 --- /dev/null +++ b/tools/web_testbench/app.py @@ -0,0 +1,55 @@ +from fastapi import FastAPI, WebSocket, Request +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse + +import uvicorn +import os +from fastapi.templating import Jinja2Templates + + +app = FastAPI() + +app.mount("/public", StaticFiles( + directory=os.path.join(os.path.dirname(__file__), "public")), name="public") + +templates = Jinja2Templates( + directory=os.path.join(os.path.dirname(__file__), "templates")) + + +class ConnectionManager: + _instance = None + + def __init__(self): + self.connections = [] + + def __new__(cls): + if not cls._instance: + cls._instance = super( + ConnectionManager, + cls + ).__new__(cls) + return cls._instance + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.connections.append(websocket) + + async def broadcast_json(self, data: str): + for connection in self.connections: + await connection.json(data) + + +@app.websocket("/ws") +async def ws(websocket: WebSocket): + await ConnectionManager().connect(websocket) + + +@app.get("/") +def dashboard(request: Request): + return templates.TemplateResponse("index.html", { + "request": request, + }) + + +if __name__ == '__main__': + uvicorn.run("app:app", host="0.0.0.0", port=80) diff --git a/tools/web_testbench/public/create_layout.js b/tools/web_testbench/public/create_layout.js new file mode 100644 index 00000000..93332a2e --- /dev/null +++ b/tools/web_testbench/public/create_layout.js @@ -0,0 +1,261 @@ +import { sio } from "./sio.js" +import { generate_graphs } from "./generate_graphs.js" + + +function pauseMiner(ip, checkbox) { + // if the checkbox is checked we need to pause, unchecked is unpause + if (checkbox.checked){ + sio.emit("pause", ip) + } else if (!(checkbox.check)) { + sio.emit("unpause", ip) + } +} + +function checkPause(ip, checkbox) { + // make sure the checkbox exists, removes an error + if (checkbox) { + // get status of pause and set checkbox to this status + sio.emit("check_pause", ip, (result) => { + checkbox.checked = result + } + ); + } +} + +function lightMiner(ip, checkbox) { + // if the checkbox is checked turn the light on, otherwise off + if (checkbox.checked){ + sio.emit("light", ip) + } else if (!(checkbox.check)) { + sio.emit("unlight", ip) + } +} + +function checkLight(ip, checkbox) { + // make sure the checkbox exists, removes an error + if (checkbox) { + // get status of light and set checkbox to this status + sio.emit("check_light", ip, (result) => { + checkbox.checked = result + } + ); + } +} + +export function generate_layout(data_graph) { + // get the container for all the charts and data + var container_all = document.getElementById('chart_container'); + // empty the container out + container_all.innerHTML = "" + + data_graph.miners.forEach(function(miner) { + + // create main div column for all data to sit inside + var column = document.createElement('div'); + column.className = "col border border-dark p-3" + + // create IP address header + var header = document.createElement('button'); + header.className = "text-center btn btn-primary w-100" + header.onclick = function(){window.open("http://" + miner.IP, '_blank');} + header.innerHTML += miner.IP + + // add the header to col first + column.append(header) + + // create light button container + var container_light = document.createElement('div'); + container_light.className = "form-check form-switch d-flex justify-content-evenly" + + // create light button + var light_switch = document.createElement('input'); + light_switch.type = "checkbox" + light_switch.id = "light_" + miner.IP + light_switch.className = "form-check-input" + + // check if the light is turned on and add click listener + checkLight(miner.IP, light_switch); + light_switch.addEventListener("click", function(){lightMiner(miner.IP, light_switch);}, false); + + // add a light label to the button + var label_light = document.createElement("label"); + label_light.setAttribute("for", "light_" + miner.IP); + label_light.innerHTML = "Light"; + + // add the button and label to the container + container_light.append(light_switch) + container_light.append(label_light) + + if (miner.hasOwnProperty('text')) { + // create text row + var row_text = document.createElement('div'); + row_text.className = "row" + + // create text container + var text_container = document.createElement('div') + text_container.className = "col w-100 p-3" + + + // create text area for data + var text_area = document.createElement('textarea'); + text_area.rows = "10" + text_area.className = "form-control" + text_area.style = "font-size: 12px" + text_area.disabled = true + text_area.readonly = true + + // add data to the text area + var text = miner.text + text += text_area.innerHTML + text_area.innerHTML = text + + // add the text area to the row + row_text.append(text_area) + + // create a row for buttons + var row_buttons = document.createElement('div'); + row_buttons.className = "row mt-3" + + // create pause button container + var container_pause = document.createElement('div'); + container_pause.className = "form-check form-switch d-flex justify-content-evenly" + + // create the pause button + var pause_switch = document.createElement('input'); + pause_switch.type = "checkbox" + pause_switch.id = "pause_" + miner.IP + pause_switch.className = "form-check-input" + + // check if it is paused and add the click listener + checkPause(miner.IP, pause_switch); + pause_switch.addEventListener("click", function(){pauseMiner(miner.IP, pause_switch);}, false); + + // add a pause label + var label_pause = document.createElement("label"); + label_pause.setAttribute("for", "pause_" + miner.IP); + label_pause.innerHTML = "Pause"; + + // add the label and button to the container + container_pause.append(pause_switch); + container_pause.append(label_pause); + text_container.append(row_text); + + // add the container to the row + row_buttons.append(container_pause); + + if (miner.Light == "show") { + // add the light container to the row + row_buttons.append(container_light) + } + + //add the row to the main column + column.append(text_container); + column.append(row_buttons); + + // add the column onto the page + container_all.append(column); + + } else { + // get fan rpm + var fan_rpm_1 = miner.Fans.fan_0.RPM; + var fan_rpm_2 = miner.Fans.fan_1.RPM; + + // create hr canvas + var hr_canvas = document.createElement('canvas'); + + // create temp canvas + var temp_canvas = document.createElement('canvas'); + + // create fan 1 title + var fan_1_title = document.createElement('p'); + fan_1_title.innerHTML += "Fan L: " + fan_rpm_1 + " RPM"; + fan_1_title.className = "text-center" + + // create fan 2 title + var fan_2_title = document.createElement('p'); + fan_2_title.innerHTML += "Fan R: " + fan_rpm_2 + " RPM"; + fan_2_title.className = "text-center" + + // create fan 1 canvas + var fan_1_canvas = document.createElement('canvas'); + + // create fan 2 canvas + var fan_2_canvas = document.createElement('canvas'); + + + // create row for hr and temp data + var row_hr = document.createElement('div'); + row_hr.className = "row" + + // create row for titles of fans + var row_fan_title = document.createElement('div'); + row_fan_title.className = "row" + + // create row for fan graphs + var row_fan = document.createElement('div'); + row_fan.className = "row" + + // create hr container + var container_col_hr = document.createElement('div'); + container_col_hr.className = "col w-50 ps-0 pe-4" + + // create temp container + var container_col_temp = document.createElement('div'); + container_col_temp.className = "col w-50 ps-0 pe-4" + + // create fan title 1 container + var container_col_title_fan_1 = document.createElement('div'); + container_col_title_fan_1.className = "col" + + // create fan title 2 container + var container_col_title_fan_2 = document.createElement('div'); + container_col_title_fan_2.className = "col" + + // create fan 1 data container + var container_col_fan_1 = document.createElement('div'); + container_col_fan_1.className = "col w-50 ps-3 pe-1" + + // create fan 2 data container + var container_col_fan_2 = document.createElement('div'); + container_col_fan_2.className = "col w-50 ps-3 pe-1" + + // append canvases to the appropriate container columns + container_col_hr.append(hr_canvas) + container_col_temp.append(temp_canvas) + container_col_title_fan_1.append(fan_1_title) + container_col_title_fan_2.append(fan_2_title) + container_col_fan_1.append(fan_1_canvas) + container_col_fan_2.append(fan_2_canvas) + + // add container columns to the correct rows + row_hr.append(container_col_hr) + row_hr.append(container_col_temp) + row_fan_title.append(container_col_title_fan_1) + row_fan_title.append(container_col_title_fan_2) + row_fan.append(container_col_fan_1) + row_fan.append(container_col_fan_2) + + // append the rows to the columns + column.append(row_hr) + column.append(row_fan_title) + column.append(row_fan) + + // create a row for buttons + var row_buttons = document.createElement('div'); + row_buttons.className = "row mt-3" + + if (miner.Light == "show") { + // add the light container to the row + row_buttons.append(container_light) + } + // add the row to the main column + column.append(row_buttons) + + // add the column to the page + container_all.append(column); + + // generate the graphs + generate_graphs(miner, hr_canvas, temp_canvas, fan_1_canvas, fan_2_canvas); + } + }); +} \ No newline at end of file diff --git a/tools/web_testbench/public/events.js b/tools/web_testbench/public/events.js new file mode 100644 index 00000000..3493d03e --- /dev/null +++ b/tools/web_testbench/public/events.js @@ -0,0 +1,7 @@ +import {generate_layout} from "./create_layout.js" + +// when miner data is sent +ws.onmessage = function(event) { + // generate the layout of the page + generate_layout(JSON.parse(event.data)); +}); diff --git a/tools/web_testbench/public/generate_graphs.js b/tools/web_testbench/public/generate_graphs.js new file mode 100644 index 00000000..aeb81eb5 --- /dev/null +++ b/tools/web_testbench/public/generate_graphs.js @@ -0,0 +1,135 @@ +import { options_hr, options_temp, options_fans } from "./graph_options.js"; + +// generate graphs used for the layout +export function generate_graphs(miner, hr_canvas, temp_canvas, fan_1_canvas, fan_2_canvas) { + + var hr_data = [] + + var count = 0 + // get data on all 3 boards + for (const board_num of [6, 7, 8]) { + // check if that board exists in the data + if (("board_" + board_num) in miner.HR) { + // set the key used to get the data + var key = "board_"+board_num + + // add the hr info to the hr_data + hr_data.push({label: board_num, data: [miner.HR[key].HR], backgroundColor: []}) + + // set the colors to be used in the graphs (shades of blue) + if (board_num == 6) { + hr_data[count].backgroundColor = ["rgba(0, 19, 97, 1)"] + } else if (board_num == 7) { + hr_data[count].backgroundColor = ["rgba(0, 84, 219, 1)"] + } else if (board_num == 8) { + hr_data[count].backgroundColor = ["rgba(36, 180, 224, 1)"] + } + count += 1 + } + } + + // create the hr chart + var chart_hr = new Chart(hr_canvas, { + type: "bar", + data: { + labels: ["Hashrate"], + // data from above + datasets: hr_data + }, + // options imported from graph_options.js + options: options_hr + }); + + + var temps_data = [] + + // get temp data for each board + for (const board_num of [6, 7, 8]) { + + // check if the board is in the keys list + if (("board_" + board_num) in miner.Temps) { + + // set the key to be used to access the data + key = "board_"+board_num + + // add chip and board temps to the temps_data along with colors + temps_data.push({label: board_num + " Chip", data: [miner.Temps[key].Chip], backgroundColor: ["rgba(6, 92, 39, 1)"]}); + temps_data.push({label: board_num + " Board", data: [miner.Temps[key].Board], backgroundColor: ["rgba(255, 15, 58, 1)"]}); + } + } + + + var chart_temp = new Chart(temp_canvas, { + type: "bar", + data: { + labels: ["Temps"], + // data from above + datasets: temps_data + }, + // options imported from graph_options.js + options: options_temp, + }); + + // get fan rpm + var fan_rpm_1 = miner.Fans.fan_0.RPM; + if (fan_rpm_1 == 0){ + var secondary_col_1 = "rgba(97, 4, 4, 1)" + } else { + var secondary_col_1 = "rgba(199, 199, 199, 1)" + } + var fan_rpm_2 = miner.Fans.fan_1.RPM; + if (fan_rpm_2 == 0){ + var secondary_col_2 = "rgba(97, 4, 4, 1)" + } else { + var secondary_col_2 = "rgba(199, 199, 199, 1)" + } + + // set the fan data to be rpm and the rest to go up to 6000 + var fan_data_1 = [fan_rpm_1, (6000-fan_rpm_1)]; + + // create the fan 1 chart + var chart_fan_1 = new Chart(fan_1_canvas, { + type: "doughnut", + data: { + labels: ["Fan L"], + datasets: [ + { + // data from above, no colors included + data: fan_data_1, + // add colors + backgroundColor: [ + "rgba(103, 0, 221, 1)", + secondary_col_1 + ] + }, + ] + }, + // options imported from graph_options.js + options: options_fans + }); + + + var fan_data_2 = [fan_rpm_2, (6000-fan_rpm_2)]; + + // create the fan 2 chart + var chart_fan_2 = new Chart(fan_2_canvas, { + type: "doughnut", + data: { + labels: ["Fan R"], + datasets: [ + { + // data from above, no colors included + data: fan_data_2, + // add colors + backgroundColor: [ + "rgba(103, 0, 221, 1)", + secondary_col_2 + ] + }, + ] + }, + // options imported from graph_options.js + options: options_fans + }); +} + diff --git a/tools/web_testbench/public/graph_options.js b/tools/web_testbench/public/graph_options.js new file mode 100644 index 00000000..3fbc89ed --- /dev/null +++ b/tools/web_testbench/public/graph_options.js @@ -0,0 +1,59 @@ +// All options for creation of graphs in ./generate_graphs.js + +export var options_hr = { + animation: { + duration: 0, + }, + responsive: true, + aspectRatio: .75, + plugins: { + legend: { + display: false, + } + }, + scales: { + y: { + ticks: { stepSize: .6 }, + min: 0, + suggestedMax: 3.6, + grid: { + color: function(context) { + if (context.tick.value == 2.4) { + return "rgba(0, 0, 0, 1)"; + } else if (context.tick.value > 2.4) { + return "rgba(103, 221, 0, 1)"; + } else if (context.tick.value < 2.4) { + return "rgba(221, 0, 103, 1)"; + } + } + } + } + } +}; + +export var options_temp = { + animation: { + duration: 0, + }, + responsive: true, + plugins: { + legend: { + display: false, + } + }, + aspectRatio: .75, +}; + +export var options_fans = { + animation: { + duration: 0, + }, + aspectRatio: 1.5, + events: [], + responsive: true, + plugins: { + legend: { + display: false, + } + } +}; \ No newline at end of file diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html new file mode 100644 index 00000000..03fae30a --- /dev/null +++ b/tools/web_testbench/templates/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + Title + + +
+
+
+
+
+ + From d905f6f414df8e819067bf23a57f78d0216c6327 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Wed, 30 Mar 2022 08:42:21 -0600 Subject: [PATCH 03/25] added temp fake data to the app for it to send to the JS side. --- tools/web_testbench/app.py | 62 ++++- tools/web_testbench/public/create_layout.js | 7 +- tools/web_testbench/templates/index.html | 273 +++++++++++++++++++- 3 files changed, 327 insertions(+), 15 deletions(-) diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index a09a22b3..8da41300 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -1,11 +1,15 @@ from fastapi import FastAPI, WebSocket, Request +from fastapi.websockets import WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse +import websockets.exceptions +import asyncio import uvicorn import os from fastapi.templating import Jinja2Templates +from tools.web_testbench import miner_network app = FastAPI() @@ -15,12 +19,31 @@ app.mount("/public", StaticFiles( templates = Jinja2Templates( directory=os.path.join(os.path.dirname(__file__), "templates")) +miner_data = { + 'IP': '192.168.1.10', + 'Light': 'show', + 'Fans': { + 'fan_0': {'RPM': 4620}, + 'fan_1': {'RPM': 4560}, + 'fan_2': {'RPM': 0}, + 'fan_3': {'RPM': 0} + }, + 'HR': { + 'board_6': {'HR': 4.85}, + 'board_7': {'HR': 0.0}, + 'board_8': {'HR': 0.81} + }, + 'Temps': { + 'board_6': {'Board': 85.6875, 'Chip': 93.0}, + 'board_7': {'Board': 0.0, 'Chip': 0.0}, + 'board_8': {'Board': 0.0, 'Chip': 0.0} + } +} + class ConnectionManager: _instance = None - - def __init__(self): - self.connections = [] + _connections = [] def __new__(cls): if not cls._instance: @@ -32,16 +55,32 @@ class ConnectionManager: async def connect(self, websocket: WebSocket): await websocket.accept() - self.connections.append(websocket) + await websocket.send_json({"miners": [str(miner) for miner in miner_network.hosts()]}) + ConnectionManager._connections.append(websocket) - async def broadcast_json(self, data: str): - for connection in self.connections: - await connection.json(data) + def disconnect(self, websocket: WebSocket): + print("Disconnected") + ConnectionManager._connections.remove(websocket) + + async def broadcast_json(self, data: dict): + for connection in ConnectionManager._connections: + try: + await connection.send_json(data) + except: + self.disconnect(connection) @app.websocket("/ws") async def ws(websocket: WebSocket): await ConnectionManager().connect(websocket) + try: + while True: + data = await websocket.receive() + except WebSocketDisconnect: + ConnectionManager().disconnect(websocket) + except RuntimeError: + ConnectionManager().disconnect(websocket) + @app.get("/") @@ -51,5 +90,14 @@ def dashboard(request: Request): }) +@app.on_event("startup") +def start_monitor(): + asyncio.create_task(monitor()) + +async def monitor(): + while True: + await ConnectionManager().broadcast_json(miner_data) + await asyncio.sleep(5) + if __name__ == '__main__': uvicorn.run("app:app", host="0.0.0.0", port=80) diff --git a/tools/web_testbench/public/create_layout.js b/tools/web_testbench/public/create_layout.js index 93332a2e..25a2c911 100644 --- a/tools/web_testbench/public/create_layout.js +++ b/tools/web_testbench/public/create_layout.js @@ -1,4 +1,3 @@ -import { sio } from "./sio.js" import { generate_graphs } from "./generate_graphs.js" @@ -42,13 +41,13 @@ function checkLight(ip, checkbox) { } } -export function generate_layout(data_graph) { +export function generate_layout(miners) { // get the container for all the charts and data var container_all = document.getElementById('chart_container'); // empty the container out container_all.innerHTML = "" - data_graph.miners.forEach(function(miner) { + miners.forEach(function(miner) { // create main div column for all data to sit inside var column = document.createElement('div'); @@ -258,4 +257,4 @@ export function generate_layout(data_graph) { generate_graphs(miner, hr_canvas, temp_canvas, fan_1_canvas, fan_2_canvas); } }); -} \ No newline at end of file +} diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index 03fae30a..7f0fffe7 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -3,13 +3,9 @@ - - - - Title @@ -19,5 +15,274 @@
+ + + From f0a8e7ba9f94f7ba38bd4c76b60135f692be4f16 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 31 Mar 2022 11:27:57 -0600 Subject: [PATCH 04/25] reformatted all files to use the Black formatting style --- API/__init__.py | 46 ++- API/bmminer.py | 44 +-- API/bosminer.py | 1 + API/btminer.py | 175 +++++----- API/cgminer.py | 53 ++- API/unknown.py | 1 + board_util.py | 4 +- config/__init__.py | 2 +- config/bos.py | 66 ++-- config_tool.py | 2 +- logger/__init__.py | 4 +- make_board_tool_exe.py | 40 ++- make_cfg_tool_exe.py | 32 +- miners/__init__.py | 30 +- miners/antminer/S9/bosminer.py | 2 +- miners/antminer/T9/hive.py | 18 +- miners/avalonminer/Avalon8/__init__.py | 302 +++++++++--------- miners/bmminer.py | 6 +- miners/bosminer.py | 43 +-- miners/btminer.py | 16 +- miners/cgminer.py | 40 +-- miners/miner_factory.py | 32 +- network/__init__.py | 15 +- network/net_range.py | 1 + settings/__init__.py | 4 +- tools/bad_board_util/__init__.py | 9 +- tools/bad_board_util/func/decorators.py | 15 +- tools/bad_board_util/func/files.py | 17 +- tools/bad_board_util/func/miners.py | 63 +++- tools/bad_board_util/func/ui.py | 24 +- tools/bad_board_util/layout.py | 107 ++++--- tools/bad_board_util/ui.py | 33 +- tools/cfg_util/cfg_util_sg/__init__.py | 7 +- tools/cfg_util/cfg_util_sg/func/decorators.py | 35 +- tools/cfg_util/cfg_util_sg/func/files.py | 46 +-- tools/cfg_util/cfg_util_sg/func/miners.py | 265 +++++++++------ tools/cfg_util/cfg_util_sg/func/parse_data.py | 34 +- tools/cfg_util/cfg_util_sg/func/ui.py | 45 ++- tools/cfg_util/cfg_util_sg/layout.py | 242 ++++++++------ tools/cfg_util/cfg_util_sg/ui.py | 159 ++++++--- tools/cfg_util/func/parse_data.py | 34 +- tools/web_monitor/_settings/__init__.py | 19 +- tools/web_monitor/_settings/func.py | 8 +- tools/web_monitor/app.py | 7 +- tools/web_monitor/dashboard/__init__.py | 2 +- tools/web_monitor/dashboard/func.py | 24 +- tools/web_monitor/dashboard/ws.py | 10 +- tools/web_monitor/miner/__init__.py | 9 +- tools/web_monitor/miner/remove.py | 2 +- tools/web_monitor/miner/ws.py | 114 ++++--- tools/web_monitor/scan/__init__.py | 7 +- tools/web_monitor/scan/func.py | 6 +- tools/web_monitor/scan/ws.py | 3 +- tools/web_monitor/template.py | 3 +- 54 files changed, 1369 insertions(+), 959 deletions(-) diff --git a/API/__init__.py b/API/__init__.py index 690ca21e..1fdd6b2a 100644 --- a/API/__init__.py +++ b/API/__init__.py @@ -42,17 +42,22 @@ class BaseMinerAPI: def get_commands(self) -> list: """Get a list of command accessible to a specific type of API on the miner.""" - return [func for func in - # each function in self - dir(self) if callable(getattr(self, func)) and - # no __ methods - not func.startswith("__") and - # remove all functions that are in this base class - func not in - [func for func in - dir(BaseMinerAPI) if callable(getattr(BaseMinerAPI, func)) - ] - ] + return [ + func + for func in + # each function in self + dir(self) + if callable(getattr(self, func)) and + # no __ methods + not func.startswith("__") and + # remove all functions that are in this base class + func + not in [ + func + for func in dir(BaseMinerAPI) + if callable(getattr(BaseMinerAPI, func)) + ] + ] async def multicommand(self, *commands: str) -> dict: """Creates and sends multiple commands as one command to the miner.""" @@ -63,9 +68,11 @@ class BaseMinerAPI: # 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} + warnings.warn( + f"""Removing incorrect command: {item} If you are sure you want to use this command please use API.send_command("{item}", ignore_errors=True) instead.""", - APIWarning) + APIWarning, + ) # standard multicommand format is "command1+command2" # doesnt work for S19 which is dealt with in the send command function command = "+".join(commands) @@ -87,7 +94,12 @@ If you are sure you want to use this command please use API.send_command("{item} logging.debug(f"{self.ip}: Received multicommand data.") return data - async def send_command(self, command: str, parameters: str or int or bool = None, ignore_errors: bool = False) -> dict: + async def send_command( + self, + command: str, + parameters: str or int or bool = None, + ignore_errors: bool = False, + ) -> dict: """Send an API command to the miner and return the result.""" try: # get reader and writer streams @@ -104,7 +116,7 @@ If you are sure you want to use this command please use API.send_command("{item} cmd["parameter"] = parameters # send the command - writer.write(json.dumps(cmd).encode('utf-8')) + writer.write(json.dumps(cmd).encode("utf-8")) await writer.drain() # instantiate data @@ -169,10 +181,10 @@ If you are sure you want to use this command please use API.send_command("{item} # 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] + str_data = data.decode("utf-8")[:-1] else: # no null byte - str_data = data.decode('utf-8') + 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() diff --git a/API/bmminer.py b/API/bmminer.py index 1699ce64..a6b11610 100644 --- a/API/bmminer.py +++ b/API/bmminer.py @@ -18,6 +18,7 @@ class BMMinerAPI(BaseMinerAPI): :param ip: The IP of the miner to reference the API on. :param port: The port to reference the API on. Default is 4028. """ + def __init__(self, ip: str, port: int = 4028) -> None: super().__init__(ip, port) @@ -115,11 +116,7 @@ class BMMinerAPI(BaseMinerAPI): """ return await self.send_command("enablepool", parameters=n) - async def addpool(self, - url: str, - username: str, - password: str - ) -> dict: + async def addpool(self, url: str, username: str, password: str) -> dict: """Add a pool to the miner. :param url: The URL of the new pool to add. @@ -128,11 +125,9 @@ class BMMinerAPI(BaseMinerAPI): :return: A confirmation of adding the pool. """ - return await self.send_command("addpool", - parameters=f"{url}, " - f"{username}, " - f"{password}" - ) + return await self.send_command( + "addpool", parameters=f"{url}, " f"{username}, " f"{password}" + ) async def poolpriority(self, *n: int) -> dict: """Set pool priority. @@ -142,8 +137,7 @@ class BMMinerAPI(BaseMinerAPI): :return: A confirmation of setting pool priority. """ pools = f"{','.join([str(item) for item in n])}" - return await self.send_command("poolpriority", - parameters=pools) + return await self.send_command("poolpriority", parameters=pools) async def poolquota(self, n: int, q: int) -> dict: """Set pool quota. @@ -153,10 +147,7 @@ class BMMinerAPI(BaseMinerAPI): :return: A confirmation of setting pool quota. """ - return await self.send_command("poolquota", - parameters=f"{n}, " - f"{q}" - ) + return await self.send_command("poolquota", parameters=f"{n}, " f"{q}") async def disablepool(self, n: int) -> dict: """Disable a pool. @@ -292,9 +283,7 @@ class BMMinerAPI(BaseMinerAPI): :return: Confirmation of setting failover-only. """ - return await self.send_command("failover-only", - parameters=failover - ) + return await self.send_command("failover-only", parameters=failover) async def coin(self) -> dict: """Get information on the current coin. @@ -337,10 +326,7 @@ class BMMinerAPI(BaseMinerAPI): :return: The results of setting config of name to n. """ - return await self.send_command("setconfig", - parameters=f"{name}, " - f"{n}" - ) + return await self.send_command("setconfig", parameters=f"{name}, " f"{n}") async def usbstats(self) -> dict: """Get stats of all USB devices except ztex. @@ -368,15 +354,11 @@ class BMMinerAPI(BaseMinerAPI): :return: Confirmation of setting PGA n with opt[,val]. """ if val: - return await self.send_command("pgaset", - parameters=f"{n}, " - f"{opt}, " - f"{val}" - ) + return await self.send_command( + "pgaset", parameters=f"{n}, " f"{opt}, " f"{val}" + ) else: - return await self.send_command("pgaset", - parameters=f"{n}, " - f"{opt}") + return await self.send_command("pgaset", parameters=f"{n}, " f"{opt}") async def zero(self, which: str, summary: bool) -> dict: """Zero a device. diff --git a/API/bosminer.py b/API/bosminer.py index 1d16ce4d..19d63b67 100644 --- a/API/bosminer.py +++ b/API/bosminer.py @@ -18,6 +18,7 @@ class BOSMinerAPI(BaseMinerAPI): :param ip: The IP of the miner to reference the API on. :param port: The port to reference the API on. Default is 4028. """ + def __init__(self, ip, port=4028): super().__init__(ip, port) diff --git a/API/btminer.py b/API/btminer.py index b2385cc5..e412d3ee 100644 --- a/API/btminer.py +++ b/API/btminer.py @@ -6,14 +6,12 @@ import binascii import base64 from passlib.handlers.md5_crypt import md5_crypt -from cryptography.hazmat.primitives.ciphers import \ - Cipher, algorithms, modes +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from API import BaseMinerAPI, APIError from settings import WHATSMINER_PWD - ### IMPORTANT ### # you need to change the password of the miners using the Whatsminer # tool, then you can set them back to admin with this tool, but they @@ -35,7 +33,7 @@ def _crypt(word: str, salt: str) -> str: :return: An MD5 hash of the word with the salt. """ # compile a standard format for the salt - standard_salt = re.compile('\s*\$(\d+)\$([\w\./]*)\$') + standard_salt = re.compile("\s*\$(\d+)\$([\w\./]*)\$") # check if the salt matches match = standard_salt.match(salt) # if the matching fails, the salt is incorrect @@ -58,7 +56,7 @@ def _add_to_16(string: str) -> bytes: length. """ while len(string) % 16 != 0: - string += '\0' + string += "\0" return str.encode(string) # return bytes @@ -74,20 +72,20 @@ def parse_btminer_priviledge_data(token_data: dict, data: dict): :return: A decoded dict version of the privileged command output. """ # get the encoded data from the dict - enc_data = data['enc'] + enc_data = data["enc"] # get the aes key from the token data - aeskey = hashlib.sha256( - token_data['host_passwd_md5'].encode() - ).hexdigest() + aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest() # unhexlify the aes key aeskey = binascii.unhexlify(aeskey.encode()) # 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")) + ret_msg = json.loads( + decryptor.update(base64.decodebytes(bytes(enc_data, encoding="utf8"))) + .rstrip(b"\0") + .decode("utf8") + ) return ret_msg @@ -104,11 +102,9 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes: :return: The encrypted privileged command to be sent to the miner. """ # add token to command - command['token'] = token_data['host_sign'] + command["token"] = token_data["host_sign"] # encode host_passwd data and get hexdigest - aeskey = hashlib.sha256( - token_data['host_passwd_md5'].encode() - ).hexdigest() + aeskey = hashlib.sha256(token_data["host_passwd_md5"].encode()).hexdigest() # unhexlify the encoded host_passwd aeskey = binascii.unhexlify(aeskey.encode()) # create a new AES key @@ -117,18 +113,16 @@ def create_privileged_cmd(token_data: dict, command: dict) -> bytes: # dump the command to json api_json_str = json.dumps(command) # encode the json command with the aes key - api_json_str_enc = base64.encodebytes( - encryptor.update( - _add_to_16( - api_json_str - ) - ) - ).decode("utf-8").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} + data_enc = {"enc": 1, "data": api_json_str_enc} # dump the labeled data to json api_packet_str = json.dumps(data_enc) - return api_packet_str.encode('utf-8') + return api_packet_str.encode("utf-8") class BTMinerAPI(BaseMinerAPI): @@ -157,16 +151,18 @@ class BTMinerAPI(BaseMinerAPI): :param port: The port to reference the API on. Default is 4028. :param pwd: The admin password of the miner. Default is admin. """ + def __init__(self, ip, port=4028, pwd: str = WHATSMINER_PWD): super().__init__(ip, port) self.admin_pwd = pwd self.current_token = None - async def send_command(self, - command: str or bytes, - parameters: str or int or bool = None, - ignore_errors: bool = False - ) -> dict: + async def send_command( + self, + command: str or bytes, + parameters: str or int or bool = None, + ignore_errors: bool = False, + ) -> dict: """Send a command to the miner API. Send a command using an asynchronous connection, load the data, @@ -187,10 +183,7 @@ class BTMinerAPI(BaseMinerAPI): command = json.dumps({"command": command}).encode("utf-8") try: # get reader and writer streams - reader, writer = await asyncio.open_connection( - str(self.ip), - self.port - ) + reader, writer = await asyncio.open_connection(str(self.ip), self.port) # handle OSError 121 except OSError as e: if e.winerror == "121": @@ -221,13 +214,10 @@ class BTMinerAPI(BaseMinerAPI): await writer.wait_closed() # check if the returned data is encoded - if 'enc' in data.keys(): + if "enc" in data.keys(): # try to parse the encoded data try: - data = parse_btminer_priviledge_data( - self.current_token, - data - ) + data = parse_btminer_priviledge_data(self.current_token, data) except Exception as e: print(e) @@ -250,25 +240,24 @@ class BTMinerAPI(BaseMinerAPI): data = await self.send_command("get_token") # encrypt the admin password with the salt - pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + '$') - pwd = pwd.split('$') + pwd = _crypt(self.admin_pwd, "$1$" + data["Msg"]["salt"] + "$") + pwd = pwd.split("$") # take the 4th item from the pwd split host_passwd_md5 = pwd[3] # encrypt the pwd with the time and new salt - tmp = _crypt(pwd[3] + data["Msg"]["time"], - "$1$" + data["Msg"]["newsalt"] + '$' - ) - tmp = tmp.split('$') + tmp = _crypt(pwd[3] + data["Msg"]["time"], "$1$" + data["Msg"]["newsalt"] + "$") + tmp = tmp.split("$") # take the 4th item from the encrypted pwd split host_sign = tmp[3] # set the current token - self.current_token = {'host_sign': host_sign, - 'host_passwd_md5': host_passwd_md5 - } + self.current_token = { + "host_sign": host_sign, + "host_passwd_md5": host_passwd_md5, + } return self.current_token #### PRIVILEGED COMMANDS #### @@ -276,19 +265,18 @@ class BTMinerAPI(BaseMinerAPI): # how to configure the Whatsminer API to # use these commands. - async def update_pools(self, - 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 - ): + async def update_pools( + self, + 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, + ): """Update the pools of the miner using the API. Update the pools of the miner using the API, only works after @@ -314,15 +302,12 @@ class BTMinerAPI(BaseMinerAPI): elif pool_2 and pool_3: command = { "cmd": "update_pools", - "pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1, - "pool2": pool_2, "worker2": worker_2, "passwd2": passwd_2, - "pool3": pool_3, "worker3": worker_3, "passwd3": passwd_3, @@ -333,10 +318,9 @@ class BTMinerAPI(BaseMinerAPI): "pool1": pool_1, "worker1": worker_1, "passwd1": passwd_1, - "pool2": pool_2, "worker2": worker_2, - "passwd2": passwd_2 + "passwd2": passwd_2, } else: command = { @@ -406,12 +390,13 @@ class BTMinerAPI(BaseMinerAPI): 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 - ): + async def set_led( + self, + color: str = "red", + period: int = 2000, + duration: int = 1000, + start: int = 0, + ): """Set the LED on the miner using the API. Set the LED on the miner using the API, only works after @@ -423,12 +408,13 @@ class BTMinerAPI(BaseMinerAPI): :param start: LED on time offset in the cycle in ms. :return: A reply informing of the status of setting the LED. """ - command = {"cmd": "set_led", - "color": color, - "period": period, - "duration": duration, - "start": start - } + command = { + "cmd": "set_led", + "color": color, + "period": period, + "duration": duration, + "start": start, + } token_data = await self.get_token() enc_command = create_privileged_cmd(token_data, command) return await self.send_command(enc_command) @@ -486,10 +472,11 @@ class BTMinerAPI(BaseMinerAPI): password. """ # check if password length is greater than 8 bytes - if len(new_pwd.encode('utf-8')) > 8: + if len(new_pwd.encode("utf-8")) > 8: return APIError( f"New password too long, the max length is 8. " - f"Password size: {len(new_pwd.encode('utf-8'))}") + f"Password size: {len(new_pwd.encode('utf-8'))}" + ) command = {"cmd": "update_pwd", "old": old_pwd, "new": new_pwd} token_data = await self.get_token() enc_command = create_privileged_cmd(token_data, command) @@ -507,9 +494,11 @@ class BTMinerAPI(BaseMinerAPI): frequency. """ if not -10 < percent < 100: - return APIError(f"Frequency % is outside of the allowed " - f"range. Please set a % between -10 and " - f"100") + return APIError( + f"Frequency % is outside of the allowed " + f"range. Please set a % between -10 and " + f"100" + ) command = {"cmd": "set_target_freq", "percent": str(percent)} token_data = await self.get_token() enc_command = create_privileged_cmd(token_data, command) @@ -596,9 +585,11 @@ class BTMinerAPI(BaseMinerAPI): """ if not 0 < percent < 100: - return APIError(f"Power PCT % is outside of the allowed " - f"range. Please set a % between 0 and " - f"100") + return APIError( + f"Power PCT % is outside of the allowed " + f"range. Please set a % between 0 and " + f"100" + ) command = {"cmd": "set_power_pct", "percent": str(percent)} token_data = await self.get_token() enc_command = create_privileged_cmd(token_data, command) @@ -618,12 +609,9 @@ class BTMinerAPI(BaseMinerAPI): :return: A reply informing of the status of pre power on. """ - if not msg == \ - "wait for adjust temp" or \ - "adjust complete" or \ - "adjust continue": + if not msg == "wait for adjust temp" or "adjust complete" or "adjust continue": return APIError( - 'Message is incorrect, please choose one of ' + "Message is incorrect, please choose one of " '["wait for adjust temp", ' '"adjust complete", ' '"adjust continue"]' @@ -632,10 +620,7 @@ class BTMinerAPI(BaseMinerAPI): complete = "true" else: complete = "false" - command = {"cmd": "pre_power_on", - "complete": complete, - "msg": msg - } + command = {"cmd": "pre_power_on", "complete": complete, "msg": msg} token_data = await self.get_token() enc_command = create_privileged_cmd(token_data, command) return await self.send_command(enc_command) diff --git a/API/cgminer.py b/API/cgminer.py index 3014d5e0..f4eee6a1 100644 --- a/API/cgminer.py +++ b/API/cgminer.py @@ -18,6 +18,7 @@ class CGMinerAPI(BaseMinerAPI): :param ip: The IP of the miner to reference the API on. :param port: The port to reference the API on. Default is 4028. """ + def __init__(self, ip, port=4028): super().__init__(ip, port) @@ -111,11 +112,7 @@ class CGMinerAPI(BaseMinerAPI): """ return await self.send_command("enablepool", parameters=n) - async def addpool(self, - url: str, - username: str, - password: str - ) -> dict: + async def addpool(self, url: str, username: str, password: str) -> dict: """Add a pool to the miner. :param url: The URL of the new pool to add. @@ -124,11 +121,9 @@ class CGMinerAPI(BaseMinerAPI): :return: A confirmation of adding the pool. """ - return await self.send_command("addpool", - parameters=f"{url}, " - f"{username}, " - f"{password}" - ) + return await self.send_command( + "addpool", parameters=f"{url}, " f"{username}, " f"{password}" + ) async def poolpriority(self, *n: int) -> dict: """Set pool priority. @@ -138,8 +133,7 @@ class CGMinerAPI(BaseMinerAPI): :return: A confirmation of setting pool priority. """ pools = f"{','.join([str(item) for item in n])}" - return await self.send_command("poolpriority", - parameters=pools) + return await self.send_command("poolpriority", parameters=pools) async def poolquota(self, n: int, q: int) -> dict: """Set pool quota. @@ -149,10 +143,7 @@ class CGMinerAPI(BaseMinerAPI): :return: A confirmation of setting pool quota. """ - return await self.send_command("poolquota", - parameters=f"{n}, " - f"{q}" - ) + return await self.send_command("poolquota", parameters=f"{n}, " f"{q}") async def disablepool(self, n: int) -> dict: """Disable a pool. @@ -288,9 +279,7 @@ class CGMinerAPI(BaseMinerAPI): :return: Confirmation of setting failover-only. """ - return await self.send_command("failover-only", - parameters=failover - ) + return await self.send_command("failover-only", parameters=failover) async def coin(self) -> dict: """Get information on the current coin. @@ -333,10 +322,7 @@ class CGMinerAPI(BaseMinerAPI): :return: The results of setting config of name to n. """ - return await self.send_command("setconfig", - parameters=f"{name}, " - f"{n}" - ) + return await self.send_command("setconfig", parameters=f"{name}, " f"{n}") async def usbstats(self) -> dict: """Get stats of all USB devices except ztex. @@ -364,12 +350,11 @@ class CGMinerAPI(BaseMinerAPI): :return: Confirmation of setting PGA n with opt[,val]. """ if val: - return await self.send_command("pgaset", parameters=f"{n}, " - f"{opt}, " - f"{val}") + return await self.send_command( + "pgaset", parameters=f"{n}, " f"{opt}, " f"{val}" + ) else: - return await self.send_command("pgaset", parameters=f"{n}, " - f"{opt}") + return await self.send_command("pgaset", parameters=f"{n}, " f"{opt}") async def zero(self, which: str, summary: bool) -> dict: """Zero a device. @@ -384,8 +369,7 @@ class CGMinerAPI(BaseMinerAPI): :return: the STATUS section with info on the zero and optional summary. """ - return await self.send_command("zero", parameters=f"{which}, " - f"{summary}") + return await self.send_command("zero", parameters=f"{which}, " f"{summary}") async def hotplug(self, n: int) -> dict: """Enable hotplug. @@ -486,12 +470,11 @@ class CGMinerAPI(BaseMinerAPI): :return: Confirmation of setting option opt to value val. """ if val: - return await self.send_command("ascset", parameters=f"{n}, " - f"{opt}, " - f"{val}") + return await self.send_command( + "ascset", parameters=f"{n}, " f"{opt}, " f"{val}" + ) else: - return await self.send_command("ascset", parameters=f"{n}, " - f"{opt}") + return await self.send_command("ascset", parameters=f"{n}, " f"{opt}") async def lcd(self) -> dict: """Get a general all-in-one status summary of the miner. diff --git a/API/unknown.py b/API/unknown.py index 0bd1cc3b..d76b35fd 100644 --- a/API/unknown.py +++ b/API/unknown.py @@ -8,6 +8,7 @@ class UnknownAPI(BaseMinerAPI): and API commands as possible (API ⋂ API), to ensure that it can be used with as many APIs as possible. """ + def __init__(self, ip, port=4028): super().__init__(ip, port) diff --git a/board_util.py b/board_util.py index 744d196f..ab342455 100644 --- a/board_util.py +++ b/board_util.py @@ -1,4 +1,4 @@ from tools.bad_board_util import main -if __name__ == '__main__': - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/config/__init__.py b/config/__init__.py index f7200345..574cfcaf 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -75,4 +75,4 @@ SAMPLE CONFIG "shutdown_duration": 3.0, # -> (default = 3.0, float, (bos: power_scaling.shutdown_duration)) } } -""" \ No newline at end of file +""" diff --git a/config/bos.py b/config/bos.py index d7e47e38..bf58984f 100644 --- a/config/bos.py +++ b/config/bos.py @@ -8,7 +8,7 @@ async def bos_config_convert(config: dict): for opt in config: if opt == "format": out_config["format"] = config[opt] - out_config["format"]["generator"] = 'upstream_config_util' + out_config["format"]["generator"] = "upstream_config_util" out_config["format"]["timestamp"] = int(time.time()) elif opt == "temp_control": out_config["temperature"] = {} @@ -47,20 +47,28 @@ async def bos_config_convert(config: dict): out_config["pool_groups"][idx]["pools"] = [] out_config["pool_groups"][idx] = {} if "name" in config[opt][idx].keys(): - out_config["pool_groups"][idx]["group_name"] = config[opt][idx]["name"] + out_config["pool_groups"][idx]["group_name"] = config[opt][idx][ + "name" + ] else: out_config["pool_groups"][idx]["group_name"] = f"group_{idx}" if "quota" in config[opt][idx].keys(): out_config["pool_groups"][idx]["quota"] = config[opt][idx]["quota"] else: out_config["pool_groups"][idx]["quota"] = 1 - out_config["pool_groups"][idx]["pools"] = [{} for _item in range(len(config[opt][idx]["pool"]))] + out_config["pool_groups"][idx]["pools"] = [ + {} for _item in range(len(config[opt][idx]["pool"])) + ] for pool_idx in range(len(config[opt][idx]["pool"])): - out_config["pool_groups"][idx]["pools"][pool_idx]["url"] = config[opt][idx]["pool"][pool_idx]["url"] - out_config["pool_groups"][idx]["pools"][pool_idx]["username"] = config[opt][idx]["pool"][pool_idx][ - "user"] - out_config["pool_groups"][idx]["pools"][pool_idx]["password"] = config[opt][idx]["pool"][pool_idx][ - "password"] + out_config["pool_groups"][idx]["pools"][pool_idx]["url"] = config[ + opt + ][idx]["pool"][pool_idx]["url"] + out_config["pool_groups"][idx]["pools"][pool_idx][ + "username" + ] = config[opt][idx]["pool"][pool_idx]["user"] + out_config["pool_groups"][idx]["pools"][pool_idx][ + "password" + ] = config[opt][idx]["pool"][pool_idx]["password"] elif opt == "autotuning": out_config["autotuning"] = {} if "enabled" in config[opt].keys(): @@ -82,15 +90,21 @@ async def bos_config_convert(config: dict): else: out_config["power_scaling"]["power_step"] = 100 if "min_psu_power_limit" in config[opt].keys(): - out_config["power_scaling"]["min_psu_power_limit"] = config[opt]["min_psu_power_limit"] + out_config["power_scaling"]["min_psu_power_limit"] = config[opt][ + "min_psu_power_limit" + ] else: out_config["power_scaling"]["min_psu_power_limit"] = 800 if "shutdown_enabled" in config[opt].keys(): - out_config["power_scaling"]["shutdown_enabled"] = config[opt]["shutdown_enabled"] + out_config["power_scaling"]["shutdown_enabled"] = config[opt][ + "shutdown_enabled" + ] else: out_config["power_scaling"]["shutdown_enabled"] = False if "shutdown_duration" in config[opt].keys(): - out_config["power_scaling"]["shutdown_duration"] = config[opt]["shutdown_duration"] + out_config["power_scaling"]["shutdown_duration"] = config[opt][ + "shutdown_duration" + ] else: out_config["power_scaling"]["shutdown_duration"] = 3.0 return yaml.dump(out_config, sort_keys=False) @@ -102,7 +116,7 @@ async def general_config_convert_bos(yaml_config): for opt in config: if opt == "format": out_config["format"] = config[opt] - out_config["format"]["generator"] = 'upstream_config_util' + out_config["format"]["generator"] = "upstream_config_util" out_config["format"]["timestamp"] = int(time.time()) elif opt == "temperature": out_config["temp_control"] = {} @@ -148,11 +162,19 @@ async def general_config_convert_bos(yaml_config): out_config["group"][idx]["quota"] = config[opt][idx]["quota"] else: out_config["group"][idx]["quota"] = 1 - out_config["group"][idx]["pool"] = [{} for _item in range(len(config[opt][idx]["pools"]))] + out_config["group"][idx]["pool"] = [ + {} for _item in range(len(config[opt][idx]["pools"])) + ] for pool_idx in range(len(config[opt][idx]["pools"])): - out_config["group"][idx]["pool"][pool_idx]["url"] = config[opt][idx]["pools"][pool_idx]["url"] - out_config["group"][idx]["pool"][pool_idx]["user"] = config[opt][idx]["pools"][pool_idx]["username"] - out_config["group"][idx]["pool"][pool_idx]["password"] = config[opt][idx]["pools"][pool_idx]["password"] + out_config["group"][idx]["pool"][pool_idx]["url"] = config[opt][ + idx + ]["pools"][pool_idx]["url"] + out_config["group"][idx]["pool"][pool_idx]["user"] = config[opt][ + idx + ]["pools"][pool_idx]["username"] + out_config["group"][idx]["pool"][pool_idx]["password"] = config[ + opt + ][idx]["pools"][pool_idx]["password"] elif opt == "autotuning": out_config["autotuning"] = {} if "enabled" in config[opt].keys(): @@ -174,15 +196,21 @@ async def general_config_convert_bos(yaml_config): else: out_config["power_scaling"]["power_step"] = 100 if "min_psu_power_limit" in config[opt].keys(): - out_config["power_scaling"]["min_psu_power_limit"] = config[opt]["min_psu_power_limit"] + out_config["power_scaling"]["min_psu_power_limit"] = config[opt][ + "min_psu_power_limit" + ] else: out_config["power_scaling"]["min_psu_power_limit"] = 800 if "shutdown_enabled" in config[opt].keys(): - out_config["power_scaling"]["shutdown_enabled"] = config[opt]["shutdown_enabled"] + out_config["power_scaling"]["shutdown_enabled"] = config[opt][ + "shutdown_enabled" + ] else: out_config["power_scaling"]["shutdown_enabled"] = False if "shutdown_duration" in config[opt].keys(): - out_config["power_scaling"]["shutdown_duration"] = config[opt]["shutdown_duration"] + out_config["power_scaling"]["shutdown_duration"] = config[opt][ + "shutdown_duration" + ] else: out_config["power_scaling"]["shutdown_duration"] = 3.0 return out_config diff --git a/config_tool.py b/config_tool.py index 89b0e085..d56bf92f 100644 --- a/config_tool.py +++ b/config_tool.py @@ -1,4 +1,4 @@ from tools.cfg_util import main -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/logger/__init__.py b/logger/__init__.py index ba2d7895..1a8de1be 100644 --- a/logger/__init__.py +++ b/logger/__init__.py @@ -5,8 +5,8 @@ from settings import DEBUG logging.basicConfig( # filename="logfile.txt", # filemode="a", - format='[%(levelname)s][%(asctime)s](%(name)s) - %(message)s', - datefmt='%x %X' + format="[%(levelname)s][%(asctime)s](%(name)s) - %(message)s", + datefmt="%x %X", ) logger = logging.getLogger() diff --git a/make_board_tool_exe.py b/make_board_tool_exe.py index e96d06a6..36e9dd8e 100644 --- a/make_board_tool_exe.py +++ b/make_board_tool_exe.py @@ -19,20 +19,26 @@ version = version.strftime("%y.%m.%d") print(version) -setup(name="UpstreamBoardUtil.exe", - version=version, - description="Upstream Data Board Utility Build", - options={ - "build_exe": { - "build_exe": f"{os.getcwd()}\\build\\board_util\\UpstreamBoardUtil-{version}-{sys.platform}\\", - "include_msvcr": True, - "add_to_path": True - }, - }, - executables=[Executable( - "board_util.py", - base=base, - icon="icon.ico", - target_name="UpstreamBoardUtil.exe" - )] - ) +setup( + name="UpstreamBoardUtil.exe", + version=version, + description="Upstream Data Board Utility Build", + options={ + "build_exe": { + "build_exe": f"{os.getcwd()}\\build\\board_util\\UpstreamBoardUtil-{version}-{sys.platform}\\", + "include_files": [ + os.path.join(os.getcwd(), "settings/settings.toml"), + ], + "include_msvcr": True, + "add_to_path": True, + }, + }, + executables=[ + Executable( + "board_util.py", + base=base, + icon="icon.ico", + target_name="UpstreamBoardUtil.exe", + ) + ], +) diff --git a/make_cfg_tool_exe.py b/make_cfg_tool_exe.py index a97e2669..680e5908 100644 --- a/make_cfg_tool_exe.py +++ b/make_cfg_tool_exe.py @@ -19,13 +19,25 @@ 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}\\", - "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")] - ) +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/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", + ) + ], +) diff --git a/miners/__init__.py b/miners/__init__.py index f311faa3..01823cff 100644 --- a/miners/__init__.py +++ b/miners/__init__.py @@ -9,7 +9,11 @@ import logging class BaseMiner: - def __init__(self, ip: str, api: BMMinerAPI or BOSMinerAPI or CGMinerAPI or BTMinerAPI or UnknownAPI) -> None: + def __init__( + self, + ip: str, + api: BMMinerAPI or BOSMinerAPI or CGMinerAPI or BTMinerAPI or UnknownAPI, + ) -> None: self.ip = ipaddress.ip_address(ip) self.uname = None self.pwd = None @@ -20,19 +24,23 @@ class BaseMiner: async def _get_ssh_connection(self) -> asyncssh.connect: """Create a new asyncssh connection""" try: - conn = await asyncssh.connect(str(self.ip), - known_hosts=None, - username=self.uname, - password=self.pwd, - server_host_key_algs=['ssh-rsa']) + conn = await asyncssh.connect( + str(self.ip), + known_hosts=None, + username=self.uname, + password=self.pwd, + server_host_key_algs=["ssh-rsa"], + ) return conn except asyncssh.misc.PermissionDenied: try: - conn = await asyncssh.connect(str(self.ip), - known_hosts=None, - username="admin", - password="admin", - server_host_key_algs=['ssh-rsa']) + conn = await asyncssh.connect( + str(self.ip), + known_hosts=None, + username="admin", + password="admin", + server_host_key_algs=["ssh-rsa"], + ) return conn except Exception as e: logging.warning(f"{self} raised an exception: {e}") diff --git a/miners/antminer/S9/bosminer.py b/miners/antminer/S9/bosminer.py index c3aed38b..ac8a5ac4 100644 --- a/miners/antminer/S9/bosminer.py +++ b/miners/antminer/S9/bosminer.py @@ -24,7 +24,7 @@ class BOSMinerS9(BOSMiner): logging.debug(f"{self}: Opening SFTP connection.") async with conn.start_sftp_client() as sftp: logging.debug(f"{self}: Opening config file.") - async with sftp.open('/etc/bosminer.toml', 'w+') as file: + async with sftp.open("/etc/bosminer.toml", "w+") as file: await file.write(toml_conf) logging.debug(f"{self}: Restarting BOSMiner") await conn.run("/etc/init.d/bosminer restart") diff --git a/miners/antminer/T9/hive.py b/miners/antminer/T9/hive.py index d97d71e5..957f09fd 100644 --- a/miners/antminer/T9/hive.py +++ b/miners/antminer/T9/hive.py @@ -13,7 +13,7 @@ class HiveonT9(BMMiner): async def get_board_info(self) -> dict: """Gets data on each board and chain in the miner.""" board_stats = await self.api.stats() - stats = board_stats['STATS'][1] + stats = board_stats["STATS"][1] boards = {} board_chains = {0: [2, 9, 10], 1: [3, 11, 12], 2: [4, 13, 14]} for idx, board in enumerate(board_chains): @@ -25,12 +25,14 @@ class HiveonT9(BMMiner): nominal = False else: nominal = True - boards[board].append({ - "chain": chain, - "chip_count": count, - "chip_status": chips, - "nominal": nominal - }) + boards[board].append( + { + "chain": chain, + "chip_count": count, + "chip_status": chips, + "nominal": nominal, + } + ) return boards async def get_bad_boards(self) -> dict: @@ -43,4 +45,4 @@ class HiveonT9(BMMiner): if board not in bad_boards.keys(): bad_boards[board] = [] bad_boards[board].append(chain) - return bad_boards \ No newline at end of file + return bad_boards diff --git a/miners/avalonminer/Avalon8/__init__.py b/miners/avalonminer/Avalon8/__init__.py index ec79049a..b1952931 100644 --- a/miners/avalonminer/Avalon8/__init__.py +++ b/miners/avalonminer/Avalon8/__init__.py @@ -7,155 +7,157 @@ class CGMinerAvalon8(CGMiner): super().__init__(ip) self.model = "Avalon 8" self.api_type = "CGMiner" - self.pattern = re.compile(r'Ver\[(?P[-0-9A-Fa-f+]+)\]\s' - 'DNA\[(?P[0-9A-Fa-f]+)\]\s' - 'Elapsed\[(?P[-0-9]+)\]\s' - 'MW\[(?P[-\s0-9]+)\]\s' - 'LW\[(?P[-0-9]+)\]\s' - 'MH\[(?P[-\s0-9]+)\]\s' - 'HW\[(?P[-0-9]+)\]\s' - 'Temp\[(?P[0-9]+)\]\s' - 'TMax\[(?P[0-9]+)\]\s' - 'Fan\[(?P[0-9]+)\]\s' - 'FanR\[(?P[0-9]+)%\]\s' - 'Vi\[(?P[-\s0-9]+)\]\s' - 'Vo\[(?P[-\s0-9]+)\]\s' - '(' - 'PLL0\[(?P[-\s0-9]+)\]\s' - 'PLL1\[(?P[-\s0-9]+)\]\s' - 'PLL2\[(?P[-\s0-9]+)\]\s' - 'PLL3\[(?P[-\s0-9]+)\]\s' - ')?' - 'GHSmm\[(?P[-.0-9]+)\]\s' - 'WU\[(?P[-.0-9]+)\]\s' - 'Freq\[(?P[.0-9]+)\]\s' - 'PG\[(?P[0-9]+)\]\s' - 'Led\[(?P0|1)\]\s' - 'MW0\[(?P[0-9\s]+)\]\s' - 'MW1\[(?P[0-9\s]+)\]\s' - 'MW2\[(?P[0-9\s]+)\]\s' - 'MW3\[(?P[0-9\s]+)\]\s' - 'TA\[(?P[0-9]+)\]\s' - 'ECHU\[(?P[0-9\s]+)\]\s' - 'ECMM\[(?P[0-9]+)\]\s.*' - 'FAC0\[(?P[-0-9]+)\]\s' - 'OC\[(?P[0-9]+)\]\s' - 'SF0\[(?P[-\s0-9]+)\]\s' - 'SF1\[(?P[-\s0-9]+)\]\s' - 'SF2\[(?P[-\s0-9]+)\]\s' - 'SF3\[(?P[-\s0-9]+)\]\s' - 'PMUV\[(?P[-\s\S*]+)\]\s' - 'PVT_T0\[(?P[-0-9\s]+)\]\s' - 'PVT_T1\[(?P[-0-9\s]+)\]\s' - 'PVT_T2\[(?P[-0-9\s]+)\]\s' - 'PVT_T3\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_0\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_1\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_2\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_3\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_4\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_5\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_6\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_7\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_8\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_9\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_10\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_11\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_12\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_13\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_14\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_15\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_16\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_17\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_18\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_19\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_20\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_21\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_22\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_23\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_24\[(?P[-0-9\s]+)\]\s' - 'PVT_V0_25\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_0\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_1\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_2\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_3\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_4\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_5\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_6\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_7\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_8\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_9\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_10\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_11\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_12\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_13\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_14\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_15\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_16\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_17\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_18\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_19\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_20\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_21\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_22\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_23\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_24\[(?P[-0-9\s]+)\]\s' - 'PVT_V1_25\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_0\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_1\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_2\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_3\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_4\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_5\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_6\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_7\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_8\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_9\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_10\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_11\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_12\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_13\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_14\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_15\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_16\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_17\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_18\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_19\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_20\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_21\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_22\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_23\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_24\[(?P[-0-9\s]+)\]\s' - 'PVT_V2_25\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_0\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_1\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_2\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_3\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_4\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_5\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_6\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_7\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_8\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_9\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_10\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_11\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_12\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_13\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_14\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_15\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_16\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_17\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_18\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_19\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_20\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_21\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_22\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_23\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_24\[(?P[-0-9\s]+)\]\s' - 'PVT_V3_25\[(?P[-0-9\s]+)\]\s' - 'FM\[(?P[0-9]+)\]\s' - 'CRC\[(?P[0-9\s]+)\]', re.X - ) + self.pattern = re.compile( + r"Ver\[(?P[-0-9A-Fa-f+]+)\]\s" + "DNA\[(?P[0-9A-Fa-f]+)\]\s" + "Elapsed\[(?P[-0-9]+)\]\s" + "MW\[(?P[-\s0-9]+)\]\s" + "LW\[(?P[-0-9]+)\]\s" + "MH\[(?P[-\s0-9]+)\]\s" + "HW\[(?P[-0-9]+)\]\s" + "Temp\[(?P[0-9]+)\]\s" + "TMax\[(?P[0-9]+)\]\s" + "Fan\[(?P[0-9]+)\]\s" + "FanR\[(?P[0-9]+)%\]\s" + "Vi\[(?P[-\s0-9]+)\]\s" + "Vo\[(?P[-\s0-9]+)\]\s" + "(" + "PLL0\[(?P[-\s0-9]+)\]\s" + "PLL1\[(?P[-\s0-9]+)\]\s" + "PLL2\[(?P[-\s0-9]+)\]\s" + "PLL3\[(?P[-\s0-9]+)\]\s" + ")?" + "GHSmm\[(?P[-.0-9]+)\]\s" + "WU\[(?P[-.0-9]+)\]\s" + "Freq\[(?P[.0-9]+)\]\s" + "PG\[(?P[0-9]+)\]\s" + "Led\[(?P0|1)\]\s" + "MW0\[(?P[0-9\s]+)\]\s" + "MW1\[(?P[0-9\s]+)\]\s" + "MW2\[(?P[0-9\s]+)\]\s" + "MW3\[(?P[0-9\s]+)\]\s" + "TA\[(?P[0-9]+)\]\s" + "ECHU\[(?P[0-9\s]+)\]\s" + "ECMM\[(?P[0-9]+)\]\s.*" + "FAC0\[(?P[-0-9]+)\]\s" + "OC\[(?P[0-9]+)\]\s" + "SF0\[(?P[-\s0-9]+)\]\s" + "SF1\[(?P[-\s0-9]+)\]\s" + "SF2\[(?P[-\s0-9]+)\]\s" + "SF3\[(?P[-\s0-9]+)\]\s" + "PMUV\[(?P[-\s\S*]+)\]\s" + "PVT_T0\[(?P[-0-9\s]+)\]\s" + "PVT_T1\[(?P[-0-9\s]+)\]\s" + "PVT_T2\[(?P[-0-9\s]+)\]\s" + "PVT_T3\[(?P[-0-9\s]+)\]\s" + "PVT_V0_0\[(?P[-0-9\s]+)\]\s" + "PVT_V0_1\[(?P[-0-9\s]+)\]\s" + "PVT_V0_2\[(?P[-0-9\s]+)\]\s" + "PVT_V0_3\[(?P[-0-9\s]+)\]\s" + "PVT_V0_4\[(?P[-0-9\s]+)\]\s" + "PVT_V0_5\[(?P[-0-9\s]+)\]\s" + "PVT_V0_6\[(?P[-0-9\s]+)\]\s" + "PVT_V0_7\[(?P[-0-9\s]+)\]\s" + "PVT_V0_8\[(?P[-0-9\s]+)\]\s" + "PVT_V0_9\[(?P[-0-9\s]+)\]\s" + "PVT_V0_10\[(?P[-0-9\s]+)\]\s" + "PVT_V0_11\[(?P[-0-9\s]+)\]\s" + "PVT_V0_12\[(?P[-0-9\s]+)\]\s" + "PVT_V0_13\[(?P[-0-9\s]+)\]\s" + "PVT_V0_14\[(?P[-0-9\s]+)\]\s" + "PVT_V0_15\[(?P[-0-9\s]+)\]\s" + "PVT_V0_16\[(?P[-0-9\s]+)\]\s" + "PVT_V0_17\[(?P[-0-9\s]+)\]\s" + "PVT_V0_18\[(?P[-0-9\s]+)\]\s" + "PVT_V0_19\[(?P[-0-9\s]+)\]\s" + "PVT_V0_20\[(?P[-0-9\s]+)\]\s" + "PVT_V0_21\[(?P[-0-9\s]+)\]\s" + "PVT_V0_22\[(?P[-0-9\s]+)\]\s" + "PVT_V0_23\[(?P[-0-9\s]+)\]\s" + "PVT_V0_24\[(?P[-0-9\s]+)\]\s" + "PVT_V0_25\[(?P[-0-9\s]+)\]\s" + "PVT_V1_0\[(?P[-0-9\s]+)\]\s" + "PVT_V1_1\[(?P[-0-9\s]+)\]\s" + "PVT_V1_2\[(?P[-0-9\s]+)\]\s" + "PVT_V1_3\[(?P[-0-9\s]+)\]\s" + "PVT_V1_4\[(?P[-0-9\s]+)\]\s" + "PVT_V1_5\[(?P[-0-9\s]+)\]\s" + "PVT_V1_6\[(?P[-0-9\s]+)\]\s" + "PVT_V1_7\[(?P[-0-9\s]+)\]\s" + "PVT_V1_8\[(?P[-0-9\s]+)\]\s" + "PVT_V1_9\[(?P[-0-9\s]+)\]\s" + "PVT_V1_10\[(?P[-0-9\s]+)\]\s" + "PVT_V1_11\[(?P[-0-9\s]+)\]\s" + "PVT_V1_12\[(?P[-0-9\s]+)\]\s" + "PVT_V1_13\[(?P[-0-9\s]+)\]\s" + "PVT_V1_14\[(?P[-0-9\s]+)\]\s" + "PVT_V1_15\[(?P[-0-9\s]+)\]\s" + "PVT_V1_16\[(?P[-0-9\s]+)\]\s" + "PVT_V1_17\[(?P[-0-9\s]+)\]\s" + "PVT_V1_18\[(?P[-0-9\s]+)\]\s" + "PVT_V1_19\[(?P[-0-9\s]+)\]\s" + "PVT_V1_20\[(?P[-0-9\s]+)\]\s" + "PVT_V1_21\[(?P[-0-9\s]+)\]\s" + "PVT_V1_22\[(?P[-0-9\s]+)\]\s" + "PVT_V1_23\[(?P[-0-9\s]+)\]\s" + "PVT_V1_24\[(?P[-0-9\s]+)\]\s" + "PVT_V1_25\[(?P[-0-9\s]+)\]\s" + "PVT_V2_0\[(?P[-0-9\s]+)\]\s" + "PVT_V2_1\[(?P[-0-9\s]+)\]\s" + "PVT_V2_2\[(?P[-0-9\s]+)\]\s" + "PVT_V2_3\[(?P[-0-9\s]+)\]\s" + "PVT_V2_4\[(?P[-0-9\s]+)\]\s" + "PVT_V2_5\[(?P[-0-9\s]+)\]\s" + "PVT_V2_6\[(?P[-0-9\s]+)\]\s" + "PVT_V2_7\[(?P[-0-9\s]+)\]\s" + "PVT_V2_8\[(?P[-0-9\s]+)\]\s" + "PVT_V2_9\[(?P[-0-9\s]+)\]\s" + "PVT_V2_10\[(?P[-0-9\s]+)\]\s" + "PVT_V2_11\[(?P[-0-9\s]+)\]\s" + "PVT_V2_12\[(?P[-0-9\s]+)\]\s" + "PVT_V2_13\[(?P[-0-9\s]+)\]\s" + "PVT_V2_14\[(?P[-0-9\s]+)\]\s" + "PVT_V2_15\[(?P[-0-9\s]+)\]\s" + "PVT_V2_16\[(?P[-0-9\s]+)\]\s" + "PVT_V2_17\[(?P[-0-9\s]+)\]\s" + "PVT_V2_18\[(?P[-0-9\s]+)\]\s" + "PVT_V2_19\[(?P[-0-9\s]+)\]\s" + "PVT_V2_20\[(?P[-0-9\s]+)\]\s" + "PVT_V2_21\[(?P[-0-9\s]+)\]\s" + "PVT_V2_22\[(?P[-0-9\s]+)\]\s" + "PVT_V2_23\[(?P[-0-9\s]+)\]\s" + "PVT_V2_24\[(?P[-0-9\s]+)\]\s" + "PVT_V2_25\[(?P[-0-9\s]+)\]\s" + "PVT_V3_0\[(?P[-0-9\s]+)\]\s" + "PVT_V3_1\[(?P[-0-9\s]+)\]\s" + "PVT_V3_2\[(?P[-0-9\s]+)\]\s" + "PVT_V3_3\[(?P[-0-9\s]+)\]\s" + "PVT_V3_4\[(?P[-0-9\s]+)\]\s" + "PVT_V3_5\[(?P[-0-9\s]+)\]\s" + "PVT_V3_6\[(?P[-0-9\s]+)\]\s" + "PVT_V3_7\[(?P[-0-9\s]+)\]\s" + "PVT_V3_8\[(?P[-0-9\s]+)\]\s" + "PVT_V3_9\[(?P[-0-9\s]+)\]\s" + "PVT_V3_10\[(?P[-0-9\s]+)\]\s" + "PVT_V3_11\[(?P[-0-9\s]+)\]\s" + "PVT_V3_12\[(?P[-0-9\s]+)\]\s" + "PVT_V3_13\[(?P[-0-9\s]+)\]\s" + "PVT_V3_14\[(?P[-0-9\s]+)\]\s" + "PVT_V3_15\[(?P[-0-9\s]+)\]\s" + "PVT_V3_16\[(?P[-0-9\s]+)\]\s" + "PVT_V3_17\[(?P[-0-9\s]+)\]\s" + "PVT_V3_18\[(?P[-0-9\s]+)\]\s" + "PVT_V3_19\[(?P[-0-9\s]+)\]\s" + "PVT_V3_20\[(?P[-0-9\s]+)\]\s" + "PVT_V3_21\[(?P[-0-9\s]+)\]\s" + "PVT_V3_22\[(?P[-0-9\s]+)\]\s" + "PVT_V3_23\[(?P[-0-9\s]+)\]\s" + "PVT_V3_24\[(?P[-0-9\s]+)\]\s" + "PVT_V3_25\[(?P[-0-9\s]+)\]\s" + "FM\[(?P[0-9]+)\]\s" + "CRC\[(?P[0-9\s]+)\]", + re.X, + ) def __repr__(self) -> str: return f"CGMinerAvalon8: {str(self.ip)}" @@ -163,7 +165,7 @@ class CGMinerAvalon8(CGMiner): def parse_estats(self, estats): for estat in estats: for key in estat: - if key[:5] == 'MM ID': + if key[:5] == "MM ID": self._parse_estat(estat, key) def _parse_estat(self, estat, key): diff --git a/miners/bmminer.py b/miners/bmminer.py index d512e2d7..281d814c 100644 --- a/miners/bmminer.py +++ b/miners/bmminer.py @@ -9,8 +9,8 @@ class BMMiner(BaseMiner): super().__init__(ip, api) self.model = None self.config = None - self.uname = 'root' - self.pwd = 'admin' + self.uname = "root" + self.pwd = "admin" def __repr__(self) -> str: return f"BMMiner: {str(self.ip)}" @@ -31,7 +31,7 @@ class BMMiner(BaseMiner): try: async with (await self._get_ssh_connection()) as conn: if conn is not None: - data = await conn.run('cat /proc/sys/kernel/hostname') + data = await conn.run("cat /proc/sys/kernel/hostname") host = data.stdout.strip() logging.debug(f"Found hostname for {self.ip}: {host}") return host diff --git a/miners/bosminer.py b/miners/bosminer.py index fddf6fba..3809978c 100644 --- a/miners/bosminer.py +++ b/miners/bosminer.py @@ -4,14 +4,15 @@ import toml from config.bos import bos_config_convert, general_config_convert_bos import logging + 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' + self.uname = "root" + self.pwd = "admin" self.nominal_chips = 63 def __repr__(self) -> str: @@ -39,13 +40,13 @@ class BOSMiner(BaseMiner): async def fault_light_on(self) -> None: """Sends command to turn on fault light on the miner.""" logging.debug(f"{self}: Sending fault_light on command.") - await self.send_ssh_command('miner fault_light on') + await self.send_ssh_command("miner fault_light on") logging.debug(f"{self}: fault_light on command completed.") async def fault_light_off(self) -> None: """Sends command to turn off fault light on the miner.""" logging.debug(f"{self}: Sending fault_light off command.") - await self.send_ssh_command('miner fault_light off') + await self.send_ssh_command("miner fault_light off") logging.debug(f"{self}: fault_light off command completed.") async def restart_backend(self): @@ -54,7 +55,7 @@ class BOSMiner(BaseMiner): async def restart_bosminer(self) -> None: """Restart bosminer hashing process.""" logging.debug(f"{self}: Sending bosminer restart command.") - await self.send_ssh_command('/etc/init.d/bosminer restart') + await self.send_ssh_command("/etc/init.d/bosminer restart") logging.debug(f"{self}: bosminer restart command completed.") async def reboot(self) -> None: @@ -69,7 +70,7 @@ class BOSMiner(BaseMiner): logging.debug(f"{self}: Opening SFTP connection.") async with conn.start_sftp_client() as sftp: logging.debug(f"{self}: Reading config file.") - async with sftp.open('/etc/bosminer.toml') as file: + async with sftp.open("/etc/bosminer.toml") as file: toml_data = toml.loads(await file.read()) logging.debug(f"{self}: Converting config file.") cfg = await bos_config_convert(toml_data) @@ -80,7 +81,7 @@ class BOSMiner(BaseMiner): try: async with (await self._get_ssh_connection()) as conn: if conn is not None: - data = await conn.run('cat /proc/sys/kernel/hostname') + data = await conn.run("cat /proc/sys/kernel/hostname") host = data.stdout.strip() logging.debug(f"Found hostname for {self.ip}: {host}") return host @@ -98,7 +99,9 @@ class BOSMiner(BaseMiner): version_data = await self.api.devdetails() if version_data: if not version_data["DEVDETAILS"] == []: - self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "") + self.model = version_data["DEVDETAILS"][0]["Model"].replace( + "Antminer ", "" + ) logging.debug(f"Found model for {self.ip}: {self.model} (BOS)") return self.model + " (BOS)" logging.warning(f"Failed to get model for miner: {self}") @@ -112,7 +115,7 @@ class BOSMiner(BaseMiner): logging.debug(f"{self}: Opening SFTP connection.") async with conn.start_sftp_client() as sftp: logging.debug(f"{self}: Opening config file.") - async with sftp.open('/etc/bosminer.toml', 'w+') as file: + async with sftp.open("/etc/bosminer.toml", "w+") as file: await file.write(toml_conf) logging.debug(f"{self}: Restarting BOSMiner") await conn.run("/etc/init.d/bosminer restart") @@ -124,21 +127,23 @@ class BOSMiner(BaseMiner): if not devdetails.get("DEVDETAILS"): print("devdetails error", devdetails) return {0: [], 1: [], 2: []} - devs = devdetails['DEVDETAILS'] + devs = devdetails["DEVDETAILS"] boards = {} offset = devs[0]["ID"] for board in devs: boards[board["ID"] - offset] = [] - if not board['Chips'] == self.nominal_chips: + if not board["Chips"] == self.nominal_chips: nominal = False else: nominal = True - boards[board["ID"] - offset].append({ - "chain": board["ID"] - offset, - "chip_count": board['Chips'], - "chip_status": "o" * board['Chips'], - "nominal": nominal - }) + boards[board["ID"] - offset].append( + { + "chain": board["ID"] - offset, + "chip_count": board["Chips"], + "chip_status": "o" * board["Chips"], + "nominal": nominal, + } + ) logging.debug(f"Found board data for {self}: {boards}") return boards @@ -158,9 +163,9 @@ class BOSMiner(BaseMiner): """Checks for and provides list for working boards.""" devs = await self.api.devdetails() bad = 0 - chains = devs['DEVDETAILS'] + chains = devs["DEVDETAILS"] for chain in chains: - if chain['Chips'] == 0: + if chain["Chips"] == 0: bad += 1 if not bad > 0: return str(self.ip) diff --git a/miners/btminer.py b/miners/btminer.py index a8908155..08c7c189 100644 --- a/miners/btminer.py +++ b/miners/btminer.py @@ -53,16 +53,18 @@ class BTMiner(BaseMiner): for board in devs: boards[board["ID"] - offset] = [] if "Effective Chips" in board.keys(): - if not board['Effective Chips'] in self.nominal_chips: + if not board["Effective Chips"] in self.nominal_chips: nominal = False else: nominal = True - boards[board["ID"] - offset].append({ - "chain": board["ID"] - offset, - "chip_count": board['Effective Chips'], - "chip_status": "o" * board['Effective Chips'], - "nominal": nominal - }) + boards[board["ID"] - offset].append( + { + "chain": board["ID"] - offset, + "chip_count": board["Effective Chips"], + "chip_status": "o" * board["Effective Chips"], + "nominal": nominal, + } + ) else: logging.warning(f"Incorrect board data from {self}: {board}") print(board) diff --git a/miners/cgminer.py b/miners/cgminer.py index 0626febc..02b86ca9 100644 --- a/miners/cgminer.py +++ b/miners/cgminer.py @@ -9,8 +9,8 @@ class CGMiner(BaseMiner): super().__init__(ip, api) self.model = None self.config = None - self.uname = 'root' - self.pwd = 'admin' + self.uname = "root" + self.pwd = "admin" def __repr__(self) -> str: return f"CGMiner: {str(self.ip)}" @@ -23,8 +23,7 @@ class CGMiner(BaseMiner): except APIError: return None if version_data: - self.model = version_data["DEVDETAILS"][0]["Model"].replace( - "Antminer ", "") + self.model = version_data["DEVDETAILS"][0]["Model"].replace("Antminer ", "") return self.model return None @@ -32,7 +31,7 @@ class CGMiner(BaseMiner): try: async with (await self._get_ssh_connection()) as conn: if conn is not None: - data = await conn.run('cat /proc/sys/kernel/hostname') + data = await conn.run("cat /proc/sys/kernel/hostname") return data.stdout.strip() else: return "?" @@ -56,33 +55,36 @@ class CGMiner(BaseMiner): await self.restart_cgminer() async def restart_cgminer(self) -> None: - commands = ['cgminer-api restart', - '/usr/bin/cgminer-monitor >/dev/null 2>&1'] - commands = ';'.join(commands) + commands = ["cgminer-api restart", "/usr/bin/cgminer-monitor >/dev/null 2>&1"] + commands = ";".join(commands) await self.send_ssh_command(commands) async def reboot(self) -> None: await self.send_ssh_command("reboot") async def start_cgminer(self) -> None: - commands = ['mkdir -p /etc/tmp/', - 'echo \"*/3 * * * * /usr/bin/cgminer-monitor\" > /etc/tmp/root', - 'crontab -u root /etc/tmp/root', - '/usr/bin/cgminer-monitor >/dev/null 2>&1'] - commands = ';'.join(commands) + commands = [ + "mkdir -p /etc/tmp/", + 'echo "*/3 * * * * /usr/bin/cgminer-monitor" > /etc/tmp/root', + "crontab -u root /etc/tmp/root", + "/usr/bin/cgminer-monitor >/dev/null 2>&1", + ] + commands = ";".join(commands) await self.send_ssh_command(commands) async def stop_cgminer(self) -> None: - commands = ['mkdir -p /etc/tmp/', - 'echo \"\" > /etc/tmp/root', - 'crontab -u root /etc/tmp/root', - 'killall cgminer'] - commands = ';'.join(commands) + commands = [ + "mkdir -p /etc/tmp/", + 'echo "" > /etc/tmp/root', + "crontab -u root /etc/tmp/root", + "killall cgminer", + ] + commands = ";".join(commands) await self.send_ssh_command(commands) async def get_config(self) -> None: async with (await self._get_ssh_connection()) as conn: - command = 'cat /etc/config/cgminer' + command = "cat /etc/config/cgminer" result = await conn.run(command, check=True) self.config = result.stdout print(str(self.config)) diff --git a/miners/miner_factory.py b/miners/miner_factory.py index 9bd6bd2e..adfb9e73 100644 --- a/miners/miner_factory.py +++ b/miners/miner_factory.py @@ -46,10 +46,7 @@ class MinerFactory: def __new__(cls): if not cls._instance: - cls._instance = super( - MinerFactory, - cls - ).__new__(cls) + cls._instance = super(MinerFactory, cls).__new__(cls) return cls._instance async def get_miner_generator(self, ips: list): @@ -221,7 +218,10 @@ class MinerFactory: model = data["VERSION"][0]["Type"] else: # make sure devdetails actually contains data, if its empty, there are no devices - if "DEVDETAILS" in data.keys() and not data["DEVDETAILS"] == []: + if ( + "DEVDETAILS" in data.keys() + and not data["DEVDETAILS"] == [] + ): # check for model, for most miners if not data["DEVDETAILS"][0]["Model"] == "": @@ -261,7 +261,7 @@ class MinerFactory: cmd = {"command": command} # send the command - writer.write(json.dumps(cmd).encode('utf-8')) + writer.write(json.dumps(cmd).encode("utf-8")) await writer.drain() # instantiate data @@ -281,10 +281,10 @@ class MinerFactory: # 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] + str_data = data.decode("utf-8")[:-1] else: # no null byte - str_data = data.decode('utf-8') + 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() @@ -321,19 +321,27 @@ class MinerFactory: if data["STATUS"][0].get("STATUS") in ["I", "S"]: # check if there are any BMMiner strings in any of the dict keys - if any("BMMiner" in string for string in data["VERSION"][0].keys()): + if any( + "BMMiner" in string for string in data["VERSION"][0].keys() + ): api = "BMMiner" # check if there are any CGMiner strings in any of the dict keys - elif any("CGMiner" in string for string in data["VERSION"][0].keys()): + elif any( + "CGMiner" in string for string in data["VERSION"][0].keys() + ): api = "CGMiner" # check if there are any BOSMiner strings in any of the dict keys - elif any("BOSminer" in string for string in data["VERSION"][0].keys()): + elif any( + "BOSminer" in string for string in data["VERSION"][0].keys() + ): api = "BOSMiner" # if all that fails, check the Description to see if it is a whatsminer - elif data.get("Description") and "whatsminer" in data.get("Description"): + elif data.get("Description") and "whatsminer" in data.get( + "Description" + ): api = "BTMiner" # return the API if we found it diff --git a/network/__init__.py b/network/__init__.py index 12be1b77..01ec4168 100644 --- a/network/__init__.py +++ b/network/__init__.py @@ -3,12 +3,17 @@ import asyncio from network.net_range import MinerNetworkRange from miners.miner_factory import MinerFactory -from settings import NETWORK_PING_RETRIES as PING_RETRIES, NETWORK_PING_TIMEOUT as PING_TIMEOUT, \ - NETWORK_SCAN_THREADS as SCAN_THREADS +from settings import ( + NETWORK_PING_RETRIES as PING_RETRIES, + NETWORK_PING_TIMEOUT as PING_TIMEOUT, + NETWORK_SCAN_THREADS as SCAN_THREADS, +) class MinerNetwork: - def __init__(self, ip_addr: str or None = None, mask: str or int or None = None) -> None: + def __init__( + self, ip_addr: str or None = None, mask: str or int or None = None + ) -> None: self.network = None self.ip_addr = ip_addr self.connected_miners = {} @@ -45,7 +50,9 @@ class MinerNetwork: subnet_mask = str(self.mask) # save the network and return it - self.network = ipaddress.ip_network(f"{default_gateway}/{subnet_mask}", strict=False) + self.network = ipaddress.ip_network( + f"{default_gateway}/{subnet_mask}", strict=False + ) return self.network async def scan_network_for_miners(self) -> None or list: diff --git a/network/net_range.py b/network/net_range.py index e5e22c8e..8e260b5a 100644 --- a/network/net_range.py +++ b/network/net_range.py @@ -9,6 +9,7 @@ class MinerNetworkRange: {ip_range_1_start}-{ip_range_1_end}, {ip_range_2_start}-{ip_range_2_end} """ + def __init__(self, ip_range: str): ip_ranges = ip_range.replace(" ", "").split(",") self.host_ips = [] diff --git a/settings/__init__.py b/settings/__init__.py index 395847fe..fcfb5b93 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -15,7 +15,9 @@ WHATSMINER_PWD = "admin" DEBUG = False try: - with open(os.path.join(os.path.dirname(__file__), "settings.toml"), "r") as settings_file: + with open( + os.path.join(os.path.dirname(__file__), "settings.toml"), "r" + ) as settings_file: settings = toml.loads(settings_file.read()) except: pass diff --git a/tools/bad_board_util/__init__.py b/tools/bad_board_util/__init__.py index 5c260e98..99c11e1c 100644 --- a/tools/bad_board_util/__init__.py +++ b/tools/bad_board_util/__init__.py @@ -7,11 +7,16 @@ import sys import logging from logger import logger + logger.info("Initializing logger for CFG Util.") # Fix bug with some whatsminers and asyncio because of a socket not being shut down: -if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'): +if ( + sys.version_info[0] == 3 + and sys.version_info[1] >= 8 + and sys.platform.startswith("win") +): asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -22,5 +27,5 @@ def main(): logging.info("Closing Board Util.") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tools/bad_board_util/func/decorators.py b/tools/bad_board_util/func/decorators.py index 61f5814f..d370b8bf 100644 --- a/tools/bad_board_util/func/decorators.py +++ b/tools/bad_board_util/func/decorators.py @@ -2,13 +2,14 @@ from tools.bad_board_util.layout import window def disable_buttons(func): - button_list = ["scan", - "import_iplist", - "export_iplist", - "select_all_ips", - "refresh_data", - "open_in_web" - ] + button_list = [ + "scan", + "import_iplist", + "export_iplist", + "select_all_ips", + "refresh_data", + "open_in_web", + ] # handle the inner function that the decorator is wrapping async def inner(*args, **kwargs): diff --git a/tools/bad_board_util/func/files.py b/tools/bad_board_util/func/files.py index 7a8514d1..165feae2 100644 --- a/tools/bad_board_util/func/files.py +++ b/tools/bad_board_util/func/files.py @@ -14,10 +14,15 @@ async def import_iplist(file_location): return else: ip_list = [] - async with aiofiles.open(file_location, mode='r') as file: + 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)] + 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)) @@ -33,11 +38,11 @@ async def export_iplist(file_location, ip_list_selected): return else: if ip_list_selected is not None and not ip_list_selected == []: - async with aiofiles.open(file_location, mode='w') as file: + 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: + 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", "") diff --git a/tools/bad_board_util/func/miners.py b/tools/bad_board_util/func/miners.py index 271440ce..af33b4b7 100644 --- a/tools/bad_board_util/func/miners.py +++ b/tools/bad_board_util/func/miners.py @@ -2,7 +2,11 @@ import asyncio import ipaddress import warnings -from tools.bad_board_util.func.ui import update_ui_with_data, update_prog_bar, set_progress_bar_len +from tools.bad_board_util.func.ui import ( + update_ui_with_data, + update_prog_bar, + set_progress_bar_len, +) from tools.bad_board_util.layout import window from miners.miner_factory import MinerFactory from tools.bad_board_util.func.decorators import disable_buttons @@ -43,7 +47,10 @@ async def refresh_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]] + 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 asyncio.create_task(update_prog_bar(progress_bar_len)) @@ -68,18 +75,29 @@ async def refresh_data(ip_list: list): board_right = "" if data_point["data"]: if 0 in data_point["data"].keys(): - board_left = " ".join([chain["chip_status"] for chain in data_point["data"][0]]).replace("o", "•") + board_left = " ".join( + [chain["chip_status"] for chain in data_point["data"][0]] + ).replace("o", "•") else: row_colors.append((ip_table_index, "white", "red")) if 1 in data_point["data"].keys(): - board_center = " ".join([chain["chip_status"] for chain in data_point["data"][1]]).replace("o", "•") + board_center = " ".join( + [chain["chip_status"] for chain in data_point["data"][1]] + ).replace("o", "•") else: row_colors.append((ip_table_index, "white", "red")) if 2 in data_point["data"].keys(): - board_right = " ".join([chain["chip_status"] for chain in data_point["data"][2]]).replace("o", "•") + board_right = " ".join( + [chain["chip_status"] for chain in data_point["data"][2]] + ).replace("o", "•") else: row_colors.append((ip_table_index, "white", "red")) - if False in [chain["nominal"] for chain in [data_point["data"][key] for key in data_point["data"].keys()][0]]: + if False in [ + chain["nominal"] + for chain in [ + data_point["data"][key] for key in data_point["data"].keys() + ][0] + ]: row_colors.append((ip_table_index, "white", "red")) else: row_colors.append((ip_table_index, "white", "red")) @@ -92,7 +110,7 @@ async def refresh_data(ip_list: list): len(board_center), board_center, len(board_right), - board_right + board_right, ] ip_table_data[ip_table_index] = data window["ip_table"].update(ip_table_data, row_colors=row_colors) @@ -134,7 +152,7 @@ async def scan_and_get_data(network): 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)) + progress_bar_len += network_size - len(miners) asyncio.create_task(update_prog_bar(progress_bar_len)) await update_ui_with_data("status", "Getting Data") row_colors = [] @@ -147,18 +165,30 @@ async def scan_and_get_data(network): board_right = "" if data_point["data"]: if 0 in data_point["data"].keys(): - board_left = " ".join([chain["chip_status"] for chain in data_point["data"][0]]).replace("o", "•") + board_left = " ".join( + [chain["chip_status"] for chain in data_point["data"][0]] + ).replace("o", "•") else: row_colors.append((ip_table_index, "bad")) if 1 in data_point["data"].keys(): - board_center = " ".join([chain["chip_status"] for chain in data_point["data"][1]]).replace("o", "•") + board_center = " ".join( + [chain["chip_status"] for chain in data_point["data"][1]] + ).replace("o", "•") else: row_colors.append((ip_table_index, "bad")) if 2 in data_point["data"].keys(): - board_right = " ".join([chain["chip_status"] for chain in data_point["data"][2]]).replace("o", "•") + board_right = " ".join( + [chain["chip_status"] for chain in data_point["data"][2]] + ).replace("o", "•") else: row_colors.append((ip_table_index, "bad")) - if False in [chain["nominal"] for board in [data_point["data"][key] for key in data_point["data"].keys()] for chain in board]: + if False in [ + chain["nominal"] + for board in [ + data_point["data"][key] for key in data_point["data"].keys() + ] + for chain in board + ]: row_colors.append((ip_table_index, "bad")) else: row_colors.append((ip_table_index, "bad")) @@ -175,7 +205,7 @@ async def scan_and_get_data(network): len(board_center), board_center_chips, len(board_right), - board_right_chips + board_right_chips, ] ip_table_data[ip_table_index] = data window["ip_table"].update(ip_table_data) @@ -190,13 +220,16 @@ async def scan_and_get_data(network): def split_chips(string, number_of_splits): k, m = divmod(len(string), number_of_splits) - return (string[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(number_of_splits)) + return ( + string[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] + for i in range(number_of_splits) + ) async def get_formatted_data(ip: ipaddress.ip_address): miner = await MinerFactory().get_miner(ip) model = await miner.get_model() - warnings.filterwarnings('ignore') + warnings.filterwarnings("ignore") board_data = await miner.get_board_info() data = {"IP": str(ip), "model": str(model), "data": board_data} return data diff --git a/tools/bad_board_util/func/ui.py b/tools/bad_board_util/func/ui.py index b79bb6d8..88c0800c 100644 --- a/tools/bad_board_util/func/ui.py +++ b/tools/bad_board_util/func/ui.py @@ -8,9 +8,7 @@ import pyperclip def table_select_all(): window["ip_table"].update( - select_rows=( - [row for row in range(len(window["ip_table"].Values))] - ) + select_rows=([row for row in range(len(window["ip_table"].Values))]) ) @@ -45,7 +43,7 @@ async def update_ui_with_data(key, message, append=False): async def update_prog_bar(amount): window["progress"].Update(amount) - percent_done = 100 * (amount / window['progress'].maxlen) + percent_done = 100 * (amount / window["progress"].maxlen) window["progress_percent"].Update(f"{round(percent_done, 2)} %") if percent_done == 100: window["progress_percent"].Update("") @@ -61,17 +59,25 @@ async def sort_data(index: int or str): if window["scan"].Disabled: return await update_ui_with_data("status", "Sorting Data") - data_list = window['ip_table'].Values + data_list = window["ip_table"].Values table = window["ip_table"].Widget all_data = [] for idx, item in enumerate(data_list): all_data.append({"data": item, "tags": table.item(int(idx) + 1)["tags"]}) # ip addresses - if 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(all_data[0]["data"][index])): - new_list = sorted(all_data, key=lambda x: ipaddress.ip_address(x["data"][index])) + if 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(all_data[0]["data"][index]), + ): + new_list = sorted( + all_data, key=lambda x: ipaddress.ip_address(x["data"][index]) + ) if all_data == new_list: - new_list = sorted(all_data, reverse=True, key=lambda x: ipaddress.ip_address(x["data"][index])) + new_list = sorted( + all_data, + reverse=True, + key=lambda x: ipaddress.ip_address(x["data"][index]), + ) # everything else, model, chips else: diff --git a/tools/bad_board_util/layout.py b/tools/bad_board_util/layout.py index f5e52d1a..252075d7 100644 --- a/tools/bad_board_util/layout.py +++ b/tools/bad_board_util/layout.py @@ -1,62 +1,63 @@ import PySimpleGUI as sg -icon_of_window = b'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' +icon_of_window = b"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" sg.set_options(font=("Liberation Mono", 10)) layout = [ - [sg.Text("", key="status", size=(13, 1)), - sg.ProgressBar(1000, key="progress", size=(83, 20), orientation='h'), - sg.Text("", key="progress_percent", size=(9, 1)), - ], - + [ + sg.Text("", key="status", size=(13, 1)), + sg.ProgressBar(1000, key="progress", size=(83, 20), orientation="h"), + sg.Text("", key="progress_percent", size=(9, 1)), + ], [sg.Text("", size=(100, 1))], - - [sg.Text('Network IP:', size=(13, 1)), - sg.InputText(key='miner_network', do_not_clear=True, size=(113, 1)), - sg.Button('Scan', key='scan'), - ], - - [sg.Text('IP List File:', size=(13, 1)), - sg.Input(key="file_iplist", do_not_clear=True, size=(113, 1)), - sg.FileBrowse(), - sg.Button('Import', key="import_iplist"), - sg.Button('Export', key="export_iplist"), - ], - - [sg.Text(" IP List:", pad=(0, 0)), - sg.Text("", key="ip_count", pad=(0, 0), size=(3, 1)), - sg.Button('ALL', key="select_all_ips"), - sg.Button("REFRESH DATA", key='refresh_data'), - sg.Button("OPEN IN WEB", key='open_in_web'), - ], - - [sg.Table( - values=[], - font=("Liberation Mono", 9), - headings=[ - "IP", - "Model", - "Total Count", - "L Count", - "Left Board Chips", - "C Count", - "Center Board Chips", - "R Count", - "Right Board Chips" - ], - row_height=45, - auto_size_columns=False, - max_col_width=15, - justification="center", - key="ip_table", - col_widths=[14, 7, 11, 5, 30, 5, 30, 5, 30], - background_color="white", - text_color="black", - size=(110, 8), - expand_x=True, - enable_click_events=True, - )] + [ + sg.Text("Network IP:", size=(13, 1)), + sg.InputText(key="miner_network", do_not_clear=True, size=(113, 1)), + sg.Button("Scan", key="scan"), + ], + [ + sg.Text("IP List File:", size=(13, 1)), + sg.Input(key="file_iplist", do_not_clear=True, size=(113, 1)), + sg.FileBrowse(), + sg.Button("Import", key="import_iplist"), + sg.Button("Export", key="export_iplist"), + ], + [ + sg.Text(" IP List:", pad=(0, 0)), + sg.Text("", key="ip_count", pad=(0, 0), size=(3, 1)), + sg.Button("ALL", key="select_all_ips"), + sg.Button("REFRESH DATA", key="refresh_data"), + sg.Button("OPEN IN WEB", key="open_in_web"), + ], + [ + sg.Table( + values=[], + font=("Liberation Mono", 9), + headings=[ + "IP", + "Model", + "Total Count", + "L Count", + "Left Board Chips", + "C Count", + "Center Board Chips", + "R Count", + "Right Board Chips", + ], + row_height=45, + auto_size_columns=False, + max_col_width=15, + justification="center", + key="ip_table", + col_widths=[14, 7, 11, 5, 30, 5, 30, 5, 30], + background_color="white", + text_color="black", + size=(110, 8), + expand_x=True, + enable_click_events=True, + ) + ], ] -window = sg.Window('Upstream Board Util', layout, icon=icon_of_window) +window = sg.Window("Upstream Board Util", layout, icon=icon_of_window) diff --git a/tools/bad_board_util/ui.py b/tools/bad_board_util/ui.py index a4dc5edc..676b95d4 100644 --- a/tools/bad_board_util/ui.py +++ b/tools/bad_board_util/ui.py @@ -19,33 +19,44 @@ async def ui(): table.bind("", lambda x: table_select_all()) while True: event, value = window.read(timeout=0) - if event in (None, 'Close', sg.WIN_CLOSED): + 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[0] == "ip_table": if event[2][0] == -1: await sort_data(event[2][1]) - if event == 'open_in_web': + 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("/") + if event == "scan": + 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']) + miner_network = MinerNetwork(value["miner_network"]) asyncio.create_task(scan_and_get_data(miner_network)) - if event == 'select_all_ips': + if event == "select_all_ips": if len(value["ip_table"]) == len(window["ip_table"].Values): window["ip_table"].update(select_rows=()) else: - window["ip_table"].update(select_rows=([row for row in range(len(window["ip_table"].Values))])) + window["ip_table"].update( + select_rows=([row for row in range(len(window["ip_table"].Values))]) + ) if event == "import_iplist": asyncio.create_task(import_iplist(value["file_iplist"])) if event == "export_iplist": - asyncio.create_task(export_iplist(value["file_iplist"], [window['ip_table'].Values[item][0] for item in value['ip_table']])) + asyncio.create_task( + export_iplist( + value["file_iplist"], + [window["ip_table"].Values[item][0] for item in value["ip_table"]], + ) + ) if event == "refresh_data": - asyncio.create_task(refresh_data([window["ip_table"].Values[item][0] for item in value["ip_table"]])) + asyncio.create_task( + refresh_data( + [window["ip_table"].Values[item][0] for item in value["ip_table"]] + ) + ) if event == "__TIMEOUT__": await asyncio.sleep(0) diff --git a/tools/cfg_util/cfg_util_sg/__init__.py b/tools/cfg_util/cfg_util_sg/__init__.py index 1648db56..1f691698 100644 --- a/tools/cfg_util/cfg_util_sg/__init__.py +++ b/tools/cfg_util/cfg_util_sg/__init__.py @@ -13,10 +13,15 @@ from tools.cfg_util.cfg_util_sg.ui import ui # initialize logger and get settings from logger import logger + logger.info("Initializing logger for CFG Util.") # Fix bug with some whatsminers and asyncio because of a socket not being shut down: -if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'): +if ( + sys.version_info[0] == 3 + and sys.version_info[1] >= 8 + and sys.platform.startswith("win") +): asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) diff --git a/tools/cfg_util/cfg_util_sg/func/decorators.py b/tools/cfg_util/cfg_util_sg/func/decorators.py index 474f3962..b27d276f 100644 --- a/tools/cfg_util/cfg_util_sg/func/decorators.py +++ b/tools/cfg_util/cfg_util_sg/func/decorators.py @@ -2,23 +2,24 @@ from tools.cfg_util.cfg_util_sg.layout import window def disable_buttons(func): - button_list = ["scan", - "import_file_config", - "export_file_config", - "import_iplist", - "export_iplist", - "export_csv", - "select_all_ips", - "refresh_data", - "open_in_web", - "reboot_miners", - "restart_miner_backend", - "import_config", - "send_config", - "light", - "generate_config", - "send_miner_ssh_command_window", - ] + button_list = [ + "scan", + "import_file_config", + "export_file_config", + "import_iplist", + "export_iplist", + "export_csv", + "select_all_ips", + "refresh_data", + "open_in_web", + "reboot_miners", + "restart_miner_backend", + "import_config", + "send_config", + "light", + "generate_config", + "send_miner_ssh_command_window", + ] # handle the inner function that the decorator is wrapping async def inner(*args, **kwargs): diff --git a/tools/cfg_util/cfg_util_sg/func/files.py b/tools/cfg_util/cfg_util_sg/func/files.py index 36a75d0d..7419c800 100644 --- a/tools/cfg_util/cfg_util_sg/func/files.py +++ b/tools/cfg_util/cfg_util_sg/func/files.py @@ -17,10 +17,15 @@ async def import_iplist(file_location): return else: ip_list = [] - async with aiofiles.open(file_location, mode='r') as file: + 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)] + 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)) @@ -36,33 +41,34 @@ async def export_csv(file_location, ip_list_selected): return else: if ip_list_selected is not None and not ip_list_selected == []: - async with aiofiles.open(file_location, mode='w') as file: + async with aiofiles.open(file_location, mode="w") as file: for item in ip_list_selected: - await file.write(str( - ", ".join([str(part).rstrip().lstrip() for part in item]) - ) + "\n") + await file.write( + str(", ".join([str(part).rstrip().lstrip() for part in item])) + + "\n" + ) else: - async with aiofiles.open(file_location, mode='w') as file: - for item in window['ip_table'].Values: - await file.write(str( - ", ".join([str(part).rstrip().lstrip() for part in item]) - ) + "\n") + async with aiofiles.open(file_location, mode="w") as file: + for item in window["ip_table"].Values: + await file.write( + str(", ".join([str(part).rstrip().lstrip() for part in item])) + + "\n" + ) 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: + 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: + 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", "") @@ -72,7 +78,7 @@ async def import_config_file(file_location): if not os.path.exists(file_location): return else: - async with aiofiles.open(file_location, mode='r') as file: + 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", "") @@ -82,9 +88,9 @@ async def export_config_file(file_location, config): await update_ui_with_data("status", "Exporting") config = toml.dumps(await general_config_convert_bos(config)) config = toml.loads(config) - config['format']['generator'] = 'upstream_config_util' - config['format']['timestamp'] = int(time.time()) + 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: + async with aiofiles.open(file_location, mode="w+") as file: await file.write(config) await update_ui_with_data("status", "") diff --git a/tools/cfg_util/cfg_util_sg/func/miners.py b/tools/cfg_util/cfg_util_sg/func/miners.py index 28c2f1f1..7ff9bfad 100644 --- a/tools/cfg_util/cfg_util_sg/func/miners.py +++ b/tools/cfg_util/cfg_util_sg/func/miners.py @@ -6,17 +6,24 @@ import logging from API import APIError from tools.cfg_util.cfg_util_sg.func.parse_data import safe_parse_api_data -from tools.cfg_util.cfg_util_sg.func.ui import update_ui_with_data, update_prog_bar, set_progress_bar_len +from tools.cfg_util.cfg_util_sg.func.ui import ( + update_ui_with_data, + update_prog_bar, + set_progress_bar_len, +) from tools.cfg_util.cfg_util_sg.layout import window from miners.miner_factory import MinerFactory from config.bos import bos_config_convert from tools.cfg_util.cfg_util_sg.func.decorators import disable_buttons -from settings import CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS, CFG_UTIL_REBOOT_THREADS as REBOOT_THREADS +from settings import ( + CFG_UTIL_CONFIG_THREADS as CONFIG_THREADS, + CFG_UTIL_REBOOT_THREADS as REBOOT_THREADS, +) async def import_config(idx): await update_ui_with_data("status", "Importing") - miner_ip = window['ip_table'].Values[idx[0]][0] + miner_ip = window["ip_table"].Values[idx[0]][0] logging.debug(f"{miner_ip}: Importing config.") miner = await MinerFactory().get_miner(ipaddress.ip_address(miner_ip)) await miner.get_config() @@ -67,10 +74,10 @@ async def miner_light(ips: list): async def flip_light(ip): - ip_list = window['ip_table'].Widget + ip_list = window["ip_table"].Widget miner = await MinerFactory().get_miner(ip) index = [item[0] for item in window["ip_table"].Values].index(ip) - index_tags = ip_list.item(index + 1)['tags'] + index_tags = ip_list.item(index + 1)["tags"] if "light" not in index_tags: index_tags.append("light") ip_list.item(index + 1, tags=index_tags) @@ -122,7 +129,8 @@ async def send_miners_ssh_commands(ips: list, command: str, ssh_cmd_window): if str(item["IP"]) in ips: proc_table_index = ips.index(str(item["IP"])) proc_table_data[proc_table_index] = [ - str(item["IP"]), return_data.replace("\n", " "), + str(item["IP"]), + return_data.replace("\n", " "), ] ssh_cmd_window["ssh_cmd_table"].update(proc_table_data) @@ -238,7 +246,10 @@ async def refresh_data(ip_list: list): await update_ui_with_data("hr_total", "") 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]] + 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 asyncio.create_task(update_prog_bar(progress_bar_len)) @@ -258,9 +269,13 @@ async def refresh_data(ip_list: list): 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["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" + data_point["user"], + str(data_point["wattage"]) + " W", ] window["ip_table"].update(ip_table_data) progress_bar_len += 1 @@ -270,8 +285,10 @@ async def refresh_data(ip_list: list): hr_idx = 3 for item, _ in enumerate(window["ip_table"].Values): if len(window["ip_table"].Values[item]) > hr_idx: - if not window["ip_table"].Values[item][hr_idx] == '': - hashrate_list.append(float(window["ip_table"].Values[item][hr_idx].replace(" TH/s ", ""))) + if not window["ip_table"].Values[item][hr_idx] == "": + hashrate_list.append( + float(window["ip_table"].Values[item][hr_idx].replace(" TH/s ", "")) + ) else: hashrate_list.append(0) else: @@ -325,7 +342,7 @@ async def scan_and_get_data(network): 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)) + progress_bar_len += network_size - len(miners) asyncio.create_task(update_prog_bar(progress_bar_len)) await update_ui_with_data("status", "Getting Data") logging.debug("Getting data on miners.") @@ -334,14 +351,22 @@ async def scan_and_get_data(network): 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["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" + 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] == ''] + 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", "") @@ -350,7 +375,7 @@ async def scan_and_get_data(network): async def get_formatted_data(ip: ipaddress.ip_address): miner = await MinerFactory().get_miner(ip) logging.debug(f"Getting data for miner: {miner.ip}") - warnings.filterwarnings('ignore') + warnings.filterwarnings("ignore") miner_data = None host = await miner.get_hostname() try: @@ -365,81 +390,144 @@ async def get_formatted_data(ip: ipaddress.ip_address): user = "?" try: - miner_data = await miner.api.multicommand("summary", "devs", "temps", "tunerstatus", "pools", "stats") + miner_data = await miner.api.multicommand( + "summary", "devs", "temps", "tunerstatus", "pools", "stats" + ) except APIError: try: # no devs command, it will fail in this case - miner_data = await miner.api.multicommand("summary", "temps", "tunerstatus", "pools", "stats") + miner_data = await miner.api.multicommand( + "summary", "temps", "tunerstatus", "pools", "stats" + ) except APIError as e: logging.warning(f"{str(ip)}: {e}") - return {'TH/s': 0, 'IP': str(miner.ip), 'model': 'Unknown', 'temp': 0, 'host': 'Unknown', 'user': 'Unknown', - 'wattage': 0} + return { + "TH/s": 0, + "IP": str(miner.ip), + "model": "Unknown", + "temp": 0, + "host": "Unknown", + "user": "Unknown", + "wattage": 0, + } if miner_data: logging.info(f"Received miner data for miner: {miner.ip}") # get all data from summary if "summary" in miner_data.keys(): - if not miner_data["summary"][0].get("SUMMARY") == [] and "SUMMARY" in miner_data["summary"][0].keys(): + if ( + not miner_data["summary"][0].get("SUMMARY") == [] + and "SUMMARY" in miner_data["summary"][0].keys() + ): # temperature data, this is the idea spot to get this - 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 "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"] # hashrate data - if 'MHS av' in miner_data['summary'][0]['SUMMARY'][0].keys(): - th5s = format(round(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'MHS av') / 1000000, 2), ".2f").rjust(6, " ") - elif 'GHS av' in miner_data['summary'][0]['SUMMARY'][0].keys(): - if not miner_data['summary'][0]['SUMMARY'][0]['GHS av'] == "": - th5s = format(round( - float(await safe_parse_api_data(miner_data, 'summary', 0, 'SUMMARY', 0, 'GHS av')) / 1000, - 2), ".2f").rjust(6, " ") + if "MHS av" in miner_data["summary"][0]["SUMMARY"][0].keys(): + th5s = format( + round( + await safe_parse_api_data( + miner_data, "summary", 0, "SUMMARY", 0, "MHS av" + ) + / 1000000, + 2, + ), + ".2f", + ).rjust(6, " ") + elif "GHS av" in miner_data["summary"][0]["SUMMARY"][0].keys(): + if not miner_data["summary"][0]["SUMMARY"][0]["GHS av"] == "": + th5s = format( + round( + float( + await safe_parse_api_data( + miner_data, "summary", 0, "SUMMARY", 0, "GHS av" + ) + ) + / 1000, + 2, + ), + ".2f", + ).rjust(6, " ") # alternate temperature data, for BraiinsOS if "temps" in miner_data.keys(): - if not miner_data["temps"][0].get('TEMPS') == []: - if "Chip" in miner_data["temps"][0]['TEMPS'][0].keys(): - for board in miner_data["temps"][0]['TEMPS']: + if not miner_data["temps"][0].get("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"] # alternate temperature data, for Whatsminers if "devs" in miner_data.keys(): - if not miner_data["devs"][0].get('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 not miner_data["devs"][0].get("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"] # alternate temperature data if "stats" in miner_data.keys(): - if not miner_data["stats"][0]['STATS'] == []: + if 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 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] # alternate temperature data, for Avalonminers - miner_data["stats"][0]['STATS'][0].keys() - if any("MM ID" in string for string in miner_data["stats"][0]['STATS'][0].keys()): + miner_data["stats"][0]["STATS"][0].keys() + if any( + "MM ID" in string + for string in miner_data["stats"][0]["STATS"][0].keys() + ): temp_all = [] - for key in [string for string in miner_data["stats"][0]['STATS'][0].keys() if "MM ID" in string]: - for value in [string for string in miner_data["stats"][0]['STATS'][0][key].split(" ") if - "TMax" in string]: + for key in [ + string + for string in miner_data["stats"][0]["STATS"][0].keys() + if "MM ID" in string + ]: + for value in [ + string + for string in miner_data["stats"][0]["STATS"][0][key].split(" ") + if "TMax" in string + ]: temp_all.append(int(value.split("[")[1].replace("]", ""))) temps = round(sum(temp_all) / len(temp_all)) # pool information if "pools" in miner_data.keys(): - if not miner_data['pools'][0].get('POOLS') == []: - user = await safe_parse_api_data(miner_data, 'pools', 0, 'POOLS', 0, 'User') + if not miner_data["pools"][0].get("POOLS") == []: + user = await safe_parse_api_data( + miner_data, "pools", 0, "POOLS", 0, "User" + ) else: - print(miner_data['pools'][0]) + print(miner_data["pools"][0]) user = "Blank" # braiins tuner status / wattage if "tunerstatus" in miner_data.keys(): - wattage = await safe_parse_api_data(miner_data, "tunerstatus", 0, 'TUNERSTATUS', 0, "PowerLimit") + 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") + wattage = await safe_parse_api_data( + miner_data, "summary", 0, "SUMMARY", 0, "Power" + ) - ret_data = {'TH/s': th5s, 'IP': str(miner.ip), 'model': model, - 'temp': round(temps), 'host': host, 'user': user, - 'wattage': wattage} + ret_data = { + "TH/s": th5s, + "IP": str(miner.ip), + "model": model, + "temp": round(temps), + "host": host, + "user": user, + "wattage": wattage, + } logging.debug(f"{ret_data}") @@ -455,46 +543,37 @@ async def generate_config(username, workername, v2_allowed): 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' + 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' + 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()) + "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 + "temp_control": { + "target_temp": 80.0, + "hot_temp": 90.0, + "dangerous_temp": 120.0, }, - 'autotuning': { - 'enabled': True, - 'psu_power_limit': 900 - } + "autotuning": {"enabled": True, "psu_power_limit": 900}, } - window['config'].update(await bos_config_convert(config)) + window["config"].update(await bos_config_convert(config)) diff --git a/tools/cfg_util/cfg_util_sg/func/parse_data.py b/tools/cfg_util/cfg_util_sg/func/parse_data.py index 385b6f5b..635818b7 100644 --- a/tools/cfg_util/cfg_util_sg/func/parse_data.py +++ b/tools/cfg_util/cfg_util_sg/func/parse_data.py @@ -4,7 +4,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: + if len(path) == idx + 1: if isinstance(path[idx], str): if isinstance(data, dict): if path[idx] in data.keys(): @@ -17,34 +17,50 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = if isinstance(path[idx], str): if isinstance(data, dict): if path[idx] in data.keys(): - parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path) + 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}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return parsed_data else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False elif isinstance(path[idx], int): if isinstance(data, list): if len(data) > path[idx]: - parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path) + 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}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return parsed_data else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False diff --git a/tools/cfg_util/cfg_util_sg/func/ui.py b/tools/cfg_util/cfg_util_sg/func/ui.py index 4de090a1..e052f355 100644 --- a/tools/cfg_util/cfg_util_sg/func/ui.py +++ b/tools/cfg_util/cfg_util_sg/func/ui.py @@ -8,9 +8,7 @@ import pyperclip def table_select_all(): window["ip_table"].update( - select_rows=( - [row for row in range(len(window["ip_table"].Values))] - ) + select_rows=([row for row in range(len(window["ip_table"].Values))]) ) @@ -40,7 +38,6 @@ def copy_from_ssh_table(table): pyperclip.copy(copy_string) - async def update_ui_with_data(key, message, append=False): if append: message = window[key].get_text() + message @@ -49,7 +46,7 @@ async def update_ui_with_data(key, message, append=False): async def update_prog_bar(amount): window["progress"].Update(amount) - percent_done = 100 * (amount / window['progress'].maxlen) + percent_done = 100 * (amount / window["progress"].maxlen) window["progress_percent"].Update(f"{round(percent_done, 2)} %") if percent_done == 100: window["progress_percent"].Update("") @@ -65,7 +62,7 @@ async def sort_data(index: int or str): if window["scan"].Disabled: return await update_ui_with_data("status", "Sorting Data") - data_list = window['ip_table'].Values + data_list = window["ip_table"].Values table = window["ip_table"].Widget all_data = [] for idx, item in enumerate(data_list): @@ -73,22 +70,42 @@ async def sort_data(index: int or str): # wattage if re.match("[0-9]* W", str(all_data[0]["data"][index])): - new_list = sorted(all_data, key=lambda x: int(x["data"][index].replace(" W", ""))) + new_list = sorted( + all_data, key=lambda x: int(x["data"][index].replace(" W", "")) + ) if all_data == new_list: - new_list = sorted(all_data, reverse=True, key=lambda x: int(x["data"][index].replace(" W", ""))) + new_list = sorted( + all_data, + reverse=True, + key=lambda x: int(x["data"][index].replace(" W", "")), + ) # hashrate elif re.match("[0-9]*\.?[0-9]* TH\/s", str(all_data[0]["data"][index])): - new_list = sorted(all_data, key=lambda x: float(x["data"][index].replace(" TH/s", ""))) + new_list = sorted( + all_data, key=lambda x: float(x["data"][index].replace(" TH/s", "")) + ) if all_data == new_list: - new_list = sorted(all_data, reverse=True, key=lambda x: float(x["data"][index].replace(" TH/s", ""))) + new_list = sorted( + all_data, + reverse=True, + key=lambda x: float(x["data"][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(all_data[0]["data"][index])): - new_list = sorted(all_data, key=lambda x: ipaddress.ip_address(x["data"][index])) + 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(all_data[0]["data"][index]), + ): + new_list = sorted( + all_data, key=lambda x: ipaddress.ip_address(x["data"][index]) + ) if all_data == new_list: - new_list = sorted(all_data, reverse=True, key=lambda x: ipaddress.ip_address(x["data"][index])) + new_list = sorted( + all_data, + reverse=True, + key=lambda x: ipaddress.ip_address(x["data"][index]), + ) # everything else, hostname, temp, and user else: diff --git a/tools/cfg_util/cfg_util_sg/layout.py b/tools/cfg_util/cfg_util_sg/layout.py index 99404a54..401f2069 100644 --- a/tools/cfg_util/cfg_util_sg/layout.py +++ b/tools/cfg_util/cfg_util_sg/layout.py @@ -3,131 +3,161 @@ import PySimpleGUI as sg sg.set_options(font=("Liberation Mono", 10)) -icon_of_window = b'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' +icon_of_window = b"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" layout = [ - [sg.Text("", key="status", size=(9, 1)), - sg.ProgressBar(1000, key="progress", size=(106, 20), orientation='h'), - sg.Text("", key="progress_percent", size=(9, 1))], - + [ + sg.Text("", key="status", size=(9, 1)), + sg.ProgressBar(1000, key="progress", size=(106, 20), orientation="h"), + sg.Text("", key="progress_percent", size=(9, 1)), + ], [sg.Text("", size=(100, 1))], - - [sg.Text('Network IP:', size=(13, 1)), - sg.InputText(key='miner_network', do_not_clear=True, size=(115, 1)), - sg.Button('Scan', key='scan')], - - [sg.Text('Config File:', size=(13, 1)), - sg.Input(key="file_config", do_not_clear=True, size=(115, 1)), - sg.FileBrowse(), - sg.Button('Import', key="import_file_config"), - sg.Button('Export', key="export_file_config")], - - [sg.Text('IP List File:', size=(13, 1)), - sg.Input(key="file_iplist", do_not_clear=True, size=(115, 1)), - sg.FileBrowse(), - sg.Button('Import', key="import_iplist"), - sg.Button('Export', key="export_iplist"), - sg.Button('Export CSV', key="export_csv")], - - - [sg.Column([ - - [sg.Column([ - - [sg.Text("IP List:", pad=(0, 0)), - sg.Text("", key="ip_count", pad=(0, 0), size=(3, 1)), - sg.Button('ALL', key="select_all_ips"), - sg.Button("REFRESH DATA", key='refresh_data'), - sg.Button("OPEN IN WEB", key='open_in_web'), - sg.Button("REBOOT", key='reboot_miners'), - sg.Button("RESTART BACKEND", key='restart_miner_backend'), - sg.Button("SEND SSH COMMAND", key='send_miner_ssh_command_window')], - - [sg.Text("HR Total: ", pad=(0, 0)), sg.Text("", key="hr_total")], - ])], - - [sg.Table( - values=[], - headings=["IP", - "Model", - "Hostname", - "Hashrate", - "Temperature", - "Current User", - "Wattage"], - auto_size_columns=False, - max_col_width=15, - justification="center", - key="ip_table", - col_widths=[15, 16, 15, 12, 12, 31, 11], - background_color="white", - text_color="black", - size=(135, 27), - expand_x=True, - enable_click_events=True, - )] - ]), - - sg.Column([ - [sg.Text("Config"), sg.Button("IMPORT", key="import_config"), - sg.Button("CONFIG", key="send_config"), - sg.Button("LIGHT", key="light"), - sg.Button("GENERATE", key="generate_config")], - - [sg.Text("")], - - [sg.Multiline(size=(50, 28), key="config", do_not_clear=True)], - - ]) + [ + sg.Text("Network IP:", size=(13, 1)), + sg.InputText(key="miner_network", do_not_clear=True, size=(115, 1)), + sg.Button("Scan", key="scan"), + ], + [ + sg.Text("Config File:", size=(13, 1)), + sg.Input(key="file_config", do_not_clear=True, size=(115, 1)), + sg.FileBrowse(), + sg.Button("Import", key="import_file_config"), + sg.Button("Export", key="export_file_config"), + ], + [ + sg.Text("IP List File:", size=(13, 1)), + sg.Input(key="file_iplist", do_not_clear=True, size=(115, 1)), + sg.FileBrowse(), + sg.Button("Import", key="import_iplist"), + sg.Button("Export", key="export_iplist"), + sg.Button("Export CSV", key="export_csv"), + ], + [ + sg.Column( + [ + [ + sg.Column( + [ + [ + sg.Text("IP List:", pad=(0, 0)), + sg.Text("", key="ip_count", pad=(0, 0), size=(3, 1)), + sg.Button("ALL", key="select_all_ips"), + sg.Button("REFRESH DATA", key="refresh_data"), + sg.Button("OPEN IN WEB", key="open_in_web"), + sg.Button("REBOOT", key="reboot_miners"), + sg.Button( + "RESTART BACKEND", key="restart_miner_backend" + ), + sg.Button( + "SEND SSH COMMAND", + key="send_miner_ssh_command_window", + ), + ], + [ + sg.Text("HR Total: ", pad=(0, 0)), + sg.Text("", key="hr_total"), + ], + ] + ) + ], + [ + sg.Table( + values=[], + headings=[ + "IP", + "Model", + "Hostname", + "Hashrate", + "Temperature", + "Current User", + "Wattage", + ], + auto_size_columns=False, + max_col_width=15, + justification="center", + key="ip_table", + col_widths=[15, 16, 15, 12, 12, 31, 11], + background_color="white", + text_color="black", + size=(135, 27), + expand_x=True, + enable_click_events=True, + ) + ], + ] + ), + sg.Column( + [ + [ + sg.Text("Config"), + sg.Button("IMPORT", key="import_config"), + sg.Button("CONFIG", key="send_config"), + sg.Button("LIGHT", key="light"), + sg.Button("GENERATE", key="generate_config"), + ], + [sg.Text("")], + [sg.Multiline(size=(50, 28), key="config", do_not_clear=True)], + ] + ), ], ] def generate_config_layout(): config_layout = [ - [sg.Text("Enter your pool username and password below to generate a config for SlushPool.")], - + [ + sg.Text( + "Enter your pool username and password below to generate a config for SlushPool." + ) + ], [sg.Text("")], - - [sg.Text('Username:', size=(19, 1)), - sg.InputText(key='generate_config_window_username', do_not_clear=True, size=(45, 1))], - - [sg.Text('Worker Name (OPT):', size=(19, 1)), - sg.InputText(key='generate_config_window_workername', do_not_clear=True, size=(45, 1))], - - [sg.Text('Allow Stratum V2?:', size=(19, 1)), - sg.Checkbox('', key='generate_config_window_allow_v2', default=True)], - - [sg.Button("Generate", key="generate_config_window_generate")] + [ + sg.Text("Username:", size=(19, 1)), + sg.InputText( + key="generate_config_window_username", do_not_clear=True, size=(45, 1) + ), + ], + [ + sg.Text("Worker Name (OPT):", size=(19, 1)), + sg.InputText( + key="generate_config_window_workername", do_not_clear=True, size=(45, 1) + ), + ], + [ + sg.Text("Allow Stratum V2?:", size=(19, 1)), + sg.Checkbox("", key="generate_config_window_allow_v2", default=True), + ], + [sg.Button("Generate", key="generate_config_window_generate")], ] return config_layout def send_ssh_cmd_layout(miner_list: list): cmd_layout = [ - [sg.Text('Command:', size=(9, 1)), - sg.InputText(key='ssh_command_window_cmd', do_not_clear=True, size=(95, 1)), - sg.Button("Send Command", key='ssh_command_window_send_cmd') + [ + sg.Text("Command:", size=(9, 1)), + sg.InputText(key="ssh_command_window_cmd", do_not_clear=True, size=(95, 1)), + sg.Button("Send Command", key="ssh_command_window_send_cmd"), ], [sg.Text("")], - [sg.Table( - values=[[ip, ""] for ip in miner_list], - headings=["IP", - "Result"], - auto_size_columns=False, - max_col_width=15, - justification="center", - key="ssh_cmd_table", - col_widths=[15, 90], - background_color="white", - text_color="black", - size=(105, 27), - expand_x=True, - enable_click_events=True, - )] - + [ + sg.Table( + values=[[ip, ""] for ip in miner_list], + headings=["IP", "Result"], + auto_size_columns=False, + max_col_width=15, + justification="center", + key="ssh_cmd_table", + col_widths=[15, 90], + background_color="white", + text_color="black", + size=(105, 27), + expand_x=True, + enable_click_events=True, + ) + ], ] return cmd_layout -window = sg.Window('Upstream Config Util', layout, icon=icon_of_window) +window = sg.Window("Upstream Config Util", layout, icon=icon_of_window) diff --git a/tools/cfg_util/cfg_util_sg/ui.py b/tools/cfg_util/cfg_util_sg/ui.py index fec9da82..bdb96814 100644 --- a/tools/cfg_util/cfg_util_sg/ui.py +++ b/tools/cfg_util/cfg_util_sg/ui.py @@ -3,13 +3,36 @@ import sys import PySimpleGUI as sg import tkinter as tk -from tools.cfg_util.cfg_util_sg.layout import window, generate_config_layout, send_ssh_cmd_layout -from tools.cfg_util.cfg_util_sg.func.miners import send_config, miner_light, refresh_data, generate_config, import_config, \ - scan_and_get_data, restart_miners_backend, reboot_miners, send_miners_ssh_commands -from tools.cfg_util.cfg_util_sg.func.files import import_iplist, \ - import_config_file, export_iplist, export_config_file, export_csv +from tools.cfg_util.cfg_util_sg.layout import ( + window, + generate_config_layout, + send_ssh_cmd_layout, +) +from tools.cfg_util.cfg_util_sg.func.miners import ( + send_config, + miner_light, + refresh_data, + generate_config, + import_config, + scan_and_get_data, + restart_miners_backend, + reboot_miners, + send_miners_ssh_commands, +) +from tools.cfg_util.cfg_util_sg.func.files import ( + import_iplist, + import_config_file, + export_iplist, + export_config_file, + export_csv, +) from tools.cfg_util.cfg_util_sg.func.decorators import disable_buttons -from tools.cfg_util.cfg_util_sg.func.ui import sort_data, copy_from_table, table_select_all, copy_from_ssh_table +from tools.cfg_util.cfg_util_sg.func.ui import ( + sort_data, + copy_from_table, + table_select_all, + copy_from_ssh_table, +) from network import MinerNetwork @@ -27,62 +50,112 @@ async def ui(): table.column(2, anchor=tk.W) while True: event, value = window.read(timeout=0) - if event in (None, 'Close', sg.WIN_CLOSED): + 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[0] == "ip_table": if event[2][0] == -1: await sort_data(event[2][1]) - if event == 'open_in_web': + 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("/") + if event == "scan": + 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']) + miner_network = MinerNetwork(value["miner_network"]) asyncio.create_task(scan_and_get_data(miner_network)) - if event == 'select_all_ips': + if event == "select_all_ips": if len(value["ip_table"]) == len(window["ip_table"].Values): window["ip_table"].update(select_rows=()) else: - 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_table']) > 0: - asyncio.create_task(import_config(value['ip_table'])) + 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_table"]) > 0: + asyncio.create_task(import_config(value["ip_table"])) if event == "restart_miner_backend": if len(window["ip_table"].Values) > 0: - asyncio.create_task(restart_miners_backend([window['ip_table'].Values[item][0] for item in value['ip_table']])) + asyncio.create_task( + restart_miners_backend( + [ + window["ip_table"].Values[item][0] + for item in value["ip_table"] + ] + ) + ) if event == "reboot_miners": if len(window["ip_table"].Values) > 0: - asyncio.create_task(reboot_miners([window['ip_table'].Values[item][0] for item in value['ip_table']])) + asyncio.create_task( + reboot_miners( + [ + window["ip_table"].Values[item][0] + for item in value["ip_table"] + ] + ) + ) if event == "send_miner_ssh_command_window": - ips = [window['ip_table'].Values[item][0] for item in value['ip_table']] + ips = [window["ip_table"].Values[item][0] for item in value["ip_table"]] if len(ips) == 0: ips = [item[0] for item in window["ip_table"].Values] if not len(ips) == 0: await generate_ssh_cmd_ui(ips) - if event == 'light': + if event == "light": if len(window["ip_table"].Values) > 0: - asyncio.create_task(miner_light([window['ip_table'].Values[item][0] for item in value['ip_table']])) + 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"], [window['ip_table'].Values[item][0] for item in value['ip_table']])) + asyncio.create_task( + export_iplist( + value["file_iplist"], + [window["ip_table"].Values[item][0] for item in value["ip_table"]], + ) + ) if event == "export_csv": - asyncio.create_task(export_csv(value["file_iplist"], [window['ip_table'].Values[item] for item in value['ip_table']])) + asyncio.create_task( + export_csv( + value["file_iplist"], + [window["ip_table"].Values[item] for item in value["ip_table"]], + ) + ) if event == "send_config": if len(window["ip_table"].Values) > 0: - asyncio.create_task(send_config([window['ip_table'].Values[item][0] for item in value['ip_table']], 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'])) + 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"])) + asyncio.create_task( + export_config_file(value["file_config"], value["config"]) + ) if event == "refresh_data": if len(window["ip_table"].Values) > 0: - asyncio.create_task(refresh_data([window["ip_table"].Values[item][0] for item in value["ip_table"]])) + asyncio.create_task( + refresh_data( + [ + window["ip_table"].Values[item][0] + for item in value["ip_table"] + ] + ) + ) if event == "generate_config": await generate_config_ui() if event == "__TIMEOUT__": @@ -90,23 +163,29 @@ async def ui(): async def generate_config_ui(): - generate_config_window = sg.Window("Generate Config", generate_config_layout(), modal=True) + 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): + 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']) + 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 @disable_buttons async def generate_ssh_cmd_ui(selected_miners: list): - ssh_cmd_window = sg.Window("Send Command", send_ssh_cmd_layout(selected_miners), modal=True) + ssh_cmd_window = sg.Window( + "Send Command", send_ssh_cmd_layout(selected_miners), modal=True + ) ssh_cmd_window.read(timeout=0) table = ssh_cmd_window["ssh_cmd_table"].Widget table.bind("", lambda x: copy_from_ssh_table(table)) @@ -114,9 +193,13 @@ async def generate_ssh_cmd_ui(selected_miners: list): table.column(1, anchor=tk.W) while True: event, values = ssh_cmd_window.read(timeout=0) - if event in (None, 'Close', sg.WIN_CLOSED): + if event in (None, "Close", sg.WIN_CLOSED): break if event == "ssh_command_window_send_cmd": - asyncio.create_task(send_miners_ssh_commands(selected_miners, values["ssh_command_window_cmd"], ssh_cmd_window)) + asyncio.create_task( + send_miners_ssh_commands( + selected_miners, values["ssh_command_window_cmd"], ssh_cmd_window + ) + ) if event == "__TIMEOUT__": await asyncio.sleep(0) diff --git a/tools/cfg_util/func/parse_data.py b/tools/cfg_util/func/parse_data.py index 385b6f5b..635818b7 100644 --- a/tools/cfg_util/func/parse_data.py +++ b/tools/cfg_util/func/parse_data.py @@ -4,7 +4,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: + if len(path) == idx + 1: if isinstance(path[idx], str): if isinstance(data, dict): if path[idx] in data.keys(): @@ -17,34 +17,50 @@ async def safe_parse_api_data(data: dict or list, *path: str or int, idx: int = if isinstance(path[idx], str): if isinstance(data, dict): if path[idx] in data.keys(): - parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path) + 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}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return parsed_data else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False elif isinstance(path[idx], int): if isinstance(data, list): if len(data) > path[idx]: - parsed_data = await safe_parse_api_data(data[path[idx]], idx=idx+1, *path) + 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}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return parsed_data else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False else: if idx == 0: - raise APIError(f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}") + raise APIError( + f"Data parsing failed on path index {idx} - \nKey: {path[idx]} \nData: {data}" + ) return False diff --git a/tools/web_monitor/_settings/__init__.py b/tools/web_monitor/_settings/__init__.py index a5ba628e..3369a2d4 100644 --- a/tools/web_monitor/_settings/__init__.py +++ b/tools/web_monitor/_settings/__init__.py @@ -11,19 +11,22 @@ router = APIRouter() @router.route("/", methods=["GET", "POST"]) async def settings(request: Request): - return templates.TemplateResponse("settings.html", { - "request": request, - "cur_miners": get_current_miner_list(), - "settings": get_current_settings() - }) + return templates.TemplateResponse( + "settings.html", + { + "request": request, + "cur_miners": get_current_miner_list(), + "settings": get_current_settings(), + }, + ) @router.post("/update") async def update_settings_page(request: Request): data = await request.form() - graph_data_sleep_time = data.get('graph_data_sleep_time') - miner_data_timeout = data.get('miner_data_timeout') - miner_identify_timeout = data.get('miner_identify_timeout') + graph_data_sleep_time = data.get("graph_data_sleep_time") + miner_data_timeout = data.get("miner_data_timeout") + miner_identify_timeout = data.get("miner_identify_timeout") new_settings = { "graph_data_sleep_time": int(graph_data_sleep_time), "miner_data_timeout": int(miner_data_timeout), diff --git a/tools/web_monitor/_settings/func.py b/tools/web_monitor/_settings/func.py index fed0d9b8..f5636751 100644 --- a/tools/web_monitor/_settings/func.py +++ b/tools/web_monitor/_settings/func.py @@ -4,7 +4,9 @@ import os def get_current_settings(): try: - with open(os.path.join(os.getcwd(), "settings/web_settings.toml"), "r") as settings_file: + with open( + os.path.join(os.getcwd(), "settings/web_settings.toml"), "r" + ) as settings_file: settings = toml.loads(settings_file.read()) except: settings = { @@ -16,5 +18,7 @@ def get_current_settings(): def update_settings(settings): - with open(os.path.join(os.getcwd(), "settings/web_settings.toml"), "w") as settings_file: + with open( + os.path.join(os.getcwd(), "settings/web_settings.toml"), "w" + ) as settings_file: settings_file.write(toml.dumps(settings)) diff --git a/tools/web_monitor/app.py b/tools/web_monitor/app.py index 515b86ca..afec77c0 100644 --- a/tools/web_monitor/app.py +++ b/tools/web_monitor/app.py @@ -12,8 +12,11 @@ from tools.web_monitor._settings import router as settings_router app = FastAPI() -app.mount("/static", StaticFiles( - directory=os.path.join(os.path.dirname(__file__), "static")), name="static") +app.mount( + "/static", + StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), + name="static", +) app.include_router(dashboard_router, tags=["dashboard"]) app.include_router(miner_router, tags=["miner"], prefix="/miner") diff --git a/tools/web_monitor/dashboard/__init__.py b/tools/web_monitor/dashboard/__init__.py index d8be703f..45e5d2ec 100644 --- a/tools/web_monitor/dashboard/__init__.py +++ b/tools/web_monitor/dashboard/__init__.py @@ -12,7 +12,7 @@ router.include_router(ws_router) @router.get("/") def index(request: Request): - return RedirectResponse(request.url_for('dashboard')) + return RedirectResponse(request.url_for("dashboard")) @router.get("/dashboard") diff --git a/tools/web_monitor/dashboard/func.py b/tools/web_monitor/dashboard/func.py index dd2cc5dc..042fda1e 100644 --- a/tools/web_monitor/dashboard/func.py +++ b/tools/web_monitor/dashboard/func.py @@ -11,23 +11,21 @@ async def get_miner_data_dashboard(miner_ip): miner_data_timeout = settings["miner_data_timeout"] miner_ip = await asyncio.wait_for( - MinerFactory().get_miner(miner_ip), - miner_identify_timeout + MinerFactory().get_miner(miner_ip), miner_identify_timeout ) miner_summary = await asyncio.wait_for( - miner_ip.api.summary(), - miner_data_timeout + miner_ip.api.summary(), miner_data_timeout ) if miner_summary: - if 'MHS av' in miner_summary['SUMMARY'][0].keys(): + if "MHS av" in miner_summary["SUMMARY"][0].keys(): hashrate = format( - round(miner_summary['SUMMARY'][0]['MHS av'] / 1000000, - 2), ".2f") - elif 'GHS av' in miner_summary['SUMMARY'][0].keys(): + round(miner_summary["SUMMARY"][0]["MHS av"] / 1000000, 2), ".2f" + ) + elif "GHS av" in miner_summary["SUMMARY"][0].keys(): hashrate = format( - round(miner_summary['SUMMARY'][0]['GHS av'] / 1000, 2), - ".2f") + round(miner_summary["SUMMARY"][0]["GHS av"] / 1000, 2), ".2f" + ) else: hashrate = 0 else: @@ -39,5 +37,7 @@ async def get_miner_data_dashboard(miner_ip): return {"ip": miner_ip, "error": "The miner is not responding."} except KeyError: - return {"ip": miner_ip, - "error": "The miner returned unusable/unsupported data."} + return { + "ip": miner_ip, + "error": "The miner returned unusable/unsupported data.", + } diff --git a/tools/web_monitor/dashboard/ws.py b/tools/web_monitor/dashboard/ws.py index 568fd74f..24bf6563 100644 --- a/tools/web_monitor/dashboard/ws.py +++ b/tools/web_monitor/dashboard/ws.py @@ -21,14 +21,18 @@ async def dashboard_websocket(websocket: WebSocket): miners = get_current_miner_list() all_miner_data = [] data_gen = asyncio.as_completed( - [get_miner_data_dashboard(miner_ip) for miner_ip in miners]) + [get_miner_data_dashboard(miner_ip) for miner_ip in miners] + ) for all_data in data_gen: data_point = await all_data all_miner_data.append(data_point) all_miner_data.sort(key=lambda x: x["ip"]) await websocket.send_json( - {"datetime": datetime.datetime.now().isoformat(), - "miners": all_miner_data}) + { + "datetime": datetime.datetime.now().isoformat(), + "miners": all_miner_data, + } + ) await asyncio.sleep(graph_sleep_time) except WebSocketDisconnect: print("Websocket disconnected.") diff --git a/tools/web_monitor/miner/__init__.py b/tools/web_monitor/miner/__init__.py index 60044cab..d4225537 100644 --- a/tools/web_monitor/miner/__init__.py +++ b/tools/web_monitor/miner/__init__.py @@ -16,8 +16,7 @@ def miner(_request: Request, _miner_ip): @router.get("/{miner_ip}") def get_miner(request: Request, miner_ip): - return templates.TemplateResponse("miner.html", { - "request": request, - "cur_miners": get_current_miner_list(), - "miner": miner_ip - }) + return templates.TemplateResponse( + "miner.html", + {"request": request, "cur_miners": get_current_miner_list(), "miner": miner_ip}, + ) diff --git a/tools/web_monitor/miner/remove.py b/tools/web_monitor/miner/remove.py index d540b69c..833632b2 100644 --- a/tools/web_monitor/miner/remove.py +++ b/tools/web_monitor/miner/remove.py @@ -13,4 +13,4 @@ def get_miner(request: Request, miner_ip): for miner_ip in miners: file.write(miner_ip + "\n") - return RedirectResponse(request.url_for('dashboard')) + return RedirectResponse(request.url_for("dashboard")) diff --git a/tools/web_monitor/miner/ws.py b/tools/web_monitor/miner/ws.py index 56fd2427..93797c3d 100644 --- a/tools/web_monitor/miner/ws.py +++ b/tools/web_monitor/miner/ws.py @@ -22,13 +22,14 @@ async def miner_websocket(websocket: WebSocket, miner_ip): while True: try: cur_miner = await asyncio.wait_for( - MinerFactory().get_miner(str(miner_ip)), - miner_identify_timeout + MinerFactory().get_miner(str(miner_ip)), miner_identify_timeout ) data = await asyncio.wait_for( - cur_miner.api.multicommand("summary", "fans", "stats", "devs", "temps"), - miner_data_timeout + cur_miner.api.multicommand( + "summary", "fans", "stats", "devs", "temps" + ), + miner_data_timeout, ) miner_model = await cur_miner.get_model() @@ -42,7 +43,8 @@ async def miner_websocket(websocket: WebSocket, miner_ip): for item in ["Fan Speed In", "Fan Speed Out"]: if item in miner_summary["SUMMARY"][0].keys(): miner_fans["FANS"].append( - {"RPM": miner_summary["SUMMARY"][0][item]}) + {"RPM": miner_summary["SUMMARY"][0][item]} + ) if "fans" in data.keys(): miner_fans = data["fans"][0] @@ -50,30 +52,52 @@ async def miner_websocket(websocket: WebSocket, miner_ip): miner_temp_list = [] if "temps" in data.keys(): miner_temps = data["temps"][0] - for board in miner_temps['TEMPS']: + for board in miner_temps["TEMPS"]: if board["Chip"] is not None and not board["Chip"] == 0.0: miner_temp_list.append(board["Chip"]) if "devs" in data.keys() and not len(miner_temp_list) > 0: - if not data["devs"][0].get('DEVS') == []: - if "Chip Temp Avg" in data["devs"][0]['DEVS'][0].keys(): - for board in data["devs"][0]['DEVS']: - if board['Chip Temp Avg'] is not None and not board['Chip Temp Avg'] == 0.0: - miner_temp_list.append(board['Chip Temp Avg']) + if not data["devs"][0].get("DEVS") == []: + if "Chip Temp Avg" in data["devs"][0]["DEVS"][0].keys(): + for board in data["devs"][0]["DEVS"]: + if ( + board["Chip Temp Avg"] is not None + and not board["Chip Temp Avg"] == 0.0 + ): + miner_temp_list.append(board["Chip Temp Avg"]) if "stats" in data.keys() and not len(miner_temp_list) > 0: - if not data["stats"][0]['STATS'] == []: + if not data["stats"][0]["STATS"] == []: for temp in ["temp2", "temp1", "temp3"]: - if temp in data["stats"][0]['STATS'][1].keys(): - if data["stats"][0]['STATS'][1][temp] is not None and not data["stats"][0]['STATS'][1][temp] == 0.0: - miner_temp_list.append(data["stats"][0]['STATS'][1][temp]) - data["stats"][0]['STATS'][0].keys() - if any("MM ID" in string for string in - data["stats"][0]['STATS'][0].keys()): + if temp in data["stats"][0]["STATS"][1].keys(): + if ( + data["stats"][0]["STATS"][1][temp] is not None + and not data["stats"][0]["STATS"][1][temp] == 0.0 + ): + miner_temp_list.append( + data["stats"][0]["STATS"][1][temp] + ) + data["stats"][0]["STATS"][0].keys() + if any( + "MM ID" in string + for string in data["stats"][0]["STATS"][0].keys() + ): temp_all = [] - for key in [string for string in data["stats"][0]['STATS'][0].keys() if "MM ID" in string]: - for value in [string for string in data["stats"][0]['STATS'][0][key].split(" ") if "TMax" in string]: - temp_all.append(int(value.split("[")[1].replace("]", ""))) + for key in [ + string + for string in data["stats"][0]["STATS"][0].keys() + if "MM ID" in string + ]: + for value in [ + string + for string in data["stats"][0]["STATS"][0][key].split( + " " + ) + if "TMax" in string + ]: + temp_all.append( + int(value.split("[")[1].replace("]", "")) + ) miner_temp_list.append(round(sum(temp_all) / len(temp_all))) if "stats" in data.keys() and not miner_fans: @@ -82,19 +106,26 @@ async def miner_websocket(websocket: WebSocket, miner_ip): for item in ["fan1", "fan2", "fan3", "fan4"]: if item in miner_stats["STATS"][1].keys(): miner_fans["FANS"].append( - {"RPM": miner_stats["STATS"][1][item]}) + {"RPM": miner_stats["STATS"][1][item]} + ) if miner_summary: - if 'MHS av' in miner_summary['SUMMARY'][0].keys(): - hashrate = float(format( - round( - miner_summary['SUMMARY'][0]['MHS av'] / 1000000, - 2), ".2f")) - elif 'GHS av' in miner_summary['SUMMARY'][0].keys(): - hashrate = float(format( - round(miner_summary['SUMMARY'][0]['GHS av'] / 1000, - 2), - ".2f")) + if "MHS av" in miner_summary["SUMMARY"][0].keys(): + hashrate = float( + format( + round( + miner_summary["SUMMARY"][0]["MHS av"] / 1000000, 2 + ), + ".2f", + ) + ) + elif "GHS av" in miner_summary["SUMMARY"][0].keys(): + hashrate = float( + format( + round(miner_summary["SUMMARY"][0]["GHS av"] / 1000, 2), + ".2f", + ) + ) else: hashrate = 0 else: @@ -111,24 +142,25 @@ async def miner_websocket(websocket: WebSocket, miner_ip): if len(miner_temp_list) == 0: miner_temps_list = [0] - data = {"hashrate": hashrate, - "fans": fan_speeds, - "temp": round(sum(miner_temp_list)/len(miner_temp_list), 2), - "datetime": datetime.datetime.now().isoformat(), - "model": miner_model} + data = { + "hashrate": hashrate, + "fans": fan_speeds, + "temp": round(sum(miner_temp_list) / len(miner_temp_list), 2), + "datetime": datetime.datetime.now().isoformat(), + "model": miner_model, + } print(data) await websocket.send_json(data) await asyncio.sleep(settings["graph_data_sleep_time"]) except asyncio.exceptions.TimeoutError: data = {"error": "The miner is not responding."} await websocket.send_json(data) - await asyncio.sleep(.5) + await asyncio.sleep(0.5) except KeyError as e: print(e) - data = { - "error": "The miner returned unusable/unsupported data."} + data = {"error": "The miner returned unusable/unsupported data."} await websocket.send_json(data) - await asyncio.sleep(.5) + await asyncio.sleep(0.5) except WebSocketDisconnect: print("Websocket disconnected.") except websockets.exceptions.ConnectionClosedOK: diff --git a/tools/web_monitor/scan/__init__.py b/tools/web_monitor/scan/__init__.py index 49aa4611..1023ca75 100644 --- a/tools/web_monitor/scan/__init__.py +++ b/tools/web_monitor/scan/__init__.py @@ -11,10 +11,9 @@ router.include_router(ws_router) @router.get("/") def scan(request: Request): - return templates.TemplateResponse("scan.html", { - "request": request, - "cur_miners": get_current_miner_list() - }) + return templates.TemplateResponse( + "scan.html", {"request": request, "cur_miners": get_current_miner_list()} + ) @router.post("/add_miners") diff --git a/tools/web_monitor/scan/func.py b/tools/web_monitor/scan/func.py index 03989d2c..f8592ea8 100644 --- a/tools/web_monitor/scan/func.py +++ b/tools/web_monitor/scan/func.py @@ -25,12 +25,14 @@ async def do_websocket_scan(websocket: WebSocket, network_ip: str): all_miners = [] async for found_miner in get_miner_generator: all_miners.append( - {"ip": found_miner.ip, "model": await found_miner.get_model()}) + {"ip": found_miner.ip, "model": await found_miner.get_model()} + ) all_miners.sort(key=lambda x: x["ip"]) send_miners = [] for miner_ip in all_miners: send_miners.append( - {"ip": str(miner_ip["ip"]), "model": miner_ip["model"]}) + {"ip": str(miner_ip["ip"]), "model": miner_ip["model"]} + ) await websocket.send_json(send_miners) await websocket.send_text("Done") except asyncio.CancelledError: diff --git a/tools/web_monitor/scan/ws.py b/tools/web_monitor/scan/ws.py index 3ead5ccf..7c4af7e5 100644 --- a/tools/web_monitor/scan/ws.py +++ b/tools/web_monitor/scan/ws.py @@ -26,8 +26,7 @@ async def websocket_scan(websocket: WebSocket): cur_task = None await websocket.send_text("Cancelled") else: - cur_task = asyncio.create_task( - do_websocket_scan(websocket, ws_data)) + cur_task = asyncio.create_task(do_websocket_scan(websocket, ws_data)) if cur_task and cur_task.done(): cur_task = None except WebSocketDisconnect: diff --git a/tools/web_monitor/template.py b/tools/web_monitor/template.py index ed50d28f..043b59a3 100644 --- a/tools/web_monitor/template.py +++ b/tools/web_monitor/template.py @@ -3,4 +3,5 @@ from fastapi.templating import Jinja2Templates templates = Jinja2Templates( - directory=os.path.join(os.path.dirname(__file__), "templates")) + directory=os.path.join(os.path.dirname(__file__), "templates") +) From a9f600b797d4258b70448df37519fd5578664d5e Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Fri, 25 Mar 2022 16:02:50 -0600 Subject: [PATCH 05/25] add base files for web interface --- tools/web_monitor/dashboard/__init__.py | 7 +++---- tools/web_testbench/app.py | 4 +--- tools/web_testbench/public/create_layout.js | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tools/web_monitor/dashboard/__init__.py b/tools/web_monitor/dashboard/__init__.py index 45e5d2ec..8c8f10d8 100644 --- a/tools/web_monitor/dashboard/__init__.py +++ b/tools/web_monitor/dashboard/__init__.py @@ -17,7 +17,6 @@ def index(request: Request): @router.get("/dashboard") def dashboard(request: Request): - return templates.TemplateResponse("index.html", { - "request": request, - "cur_miners": get_current_miner_list() - }) + return templates.TemplateResponse( + "index.html", {"request": request, "cur_miners": get_current_miner_list()} + ) diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 8da41300..25168c8f 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -1,9 +1,7 @@ from fastapi import FastAPI, WebSocket, Request from fastapi.websockets import WebSocketDisconnect -from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse -import websockets.exceptions import asyncio +from fastapi.staticfiles import StaticFiles import uvicorn import os diff --git a/tools/web_testbench/public/create_layout.js b/tools/web_testbench/public/create_layout.js index 25a2c911..1ce12b86 100644 --- a/tools/web_testbench/public/create_layout.js +++ b/tools/web_testbench/public/create_layout.js @@ -257,4 +257,5 @@ export function generate_layout(miners) { generate_graphs(miner, hr_canvas, temp_canvas, fan_1_canvas, fan_2_canvas); } }); +<<<<<<< HEAD } From ce48ae020b29df9d373fca35822164412229eff5 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 11 Apr 2022 16:13:04 -0600 Subject: [PATCH 06/25] added feeds auto-updater for web testbench --- miners/bmminer.py | 9 +++ tools/web_testbench/feeds.py | 143 +++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 tools/web_testbench/feeds.py diff --git a/miners/bmminer.py b/miners/bmminer.py index 281d814c..cb636466 100644 --- a/miners/bmminer.py +++ b/miners/bmminer.py @@ -55,6 +55,15 @@ class BMMiner(BaseMiner): continue return result + async def get_config(self): + pools = await self.api.pools() + pool_data = [] + if not pools: + return + for pool in pools["POOLS"]: + pool_data.append({"url": pool["URL"], "user": pool["User"], "pwd": "123"}) + return pool_data + async def reboot(self) -> None: logging.debug(f"{self}: Sending reboot command.") await self.send_ssh_command("reboot") diff --git a/tools/web_testbench/feeds.py b/tools/web_testbench/feeds.py new file mode 100644 index 00000000..ee532f9a --- /dev/null +++ b/tools/web_testbench/feeds.py @@ -0,0 +1,143 @@ +import aiohttp +import shutil +import aiofiles +import asyncio +from bs4 import BeautifulSoup +import re +import os +import logging + + +async def get_latest_version(session): + feeds_url = "http://feeds.braiins-os.com" + + async with session.get(feeds_url) as resp: + data = await resp.read() + + soup = BeautifulSoup(data, "html.parser") + + versions = [] + + for link in soup.find_all("td", {"class": "link"}): + link_title = link.text.strip("/") + if re.match("(\d+)\.(\d+)(\.\d+)?", link_title): + versions.append(link_title) + + versions = sorted(versions, reverse=True) + + latest_version = versions[0] + return latest_version + + +async def get_feeds_file(session, version): + feeds_url = "http://feeds.braiins-os.com" + + async with session.get(feeds_url + "/" + version) as resp: + data = await resp.read() + + soup = BeautifulSoup(data, "html.parser") + + file = None + + for link in soup.find_all("a", href=True): + href = link["href"] + if re.match("braiins-os_am1-s9_ssh_.+\.tar.gz", href): + if not href.endswith(".asc"): + file = href + + if file: + return file + + +async def get_update_file(session, version): + feeds_url = "http://feeds.braiins-os.com" + + async with session.get(feeds_url + "/am1-s9") as resp: + data = await resp.read() + + soup = BeautifulSoup(data, "html.parser") + + file = None + + for link in soup.find_all("a", href=True): + href = link["href"] + if re.match(f"firmware_(.+)-{version}-plus_arm_cortex-a9_neon\.tar", href): + if not href.endswith(".asc"): + file = href + + if file: + return file + + +async def get_latest_update_file(session, update_file): + update_file_loc = f"http://feeds.braiins-os.com/am1-s9/{update_file}" + + update_file_dir = os.path.join(os.path.dirname(__file__), "files", "update.tar") + + if os.path.exists(update_file_dir): + os.remove(update_file_dir) + + async with session.get(update_file_loc) as update_file_data: + if update_file_data.status == 200: + f = await aiofiles.open( + os.path.join(os.path.dirname(__file__), "files", "update.tar"), + mode="wb", + ) + await f.write(await update_file_data.read()) + await f.close() + + +async def get_latest_install_file(session, version, feeds_path, install_file): + install_file_loc = f"http://feeds.braiins-os.com/{version}/{install_file}" + feeds_file_path = os.path.join(feeds_path, "toolbox_bos_am1-s9") + + with open(feeds_file_path, "a+") as feeds_file: + feeds_file.write(version + "\t" + install_file) + + install_file_folder = os.path.join(feeds_path, version) + if os.path.exists(install_file_folder): + shutil.rmtree(install_file_folder) + os.mkdir(install_file_folder) + + async with session.get(install_file_loc) as install_file_data: + if install_file_data.status == 200: + f = await aiofiles.open( + os.path.join(install_file_folder, install_file), mode="wb" + ) + await f.write(await install_file_data.read()) + await f.close() + + +async def update_installer_files(): + feeds_versions = [] + feeds_path = os.path.join( + os.path.dirname(__file__), "files", "bos-toolbox", "feeds" + ) + if not os.path.exists(feeds_path): + os.mkdir(feeds_path) + + feeds_file_path = os.path.join(feeds_path, "toolbox_bos_am1-s9") + + if not os.path.exists(feeds_file_path): + feeds_file = open(feeds_file_path, "w+") + feeds_file.close() + + with open(feeds_file_path) as feeds_file: + for line in feeds_file.readlines(): + ver = line.strip().split("\t")[0] + feeds_versions.append(ver) + + async with aiohttp.ClientSession() as session: + version = await get_latest_version(session) + + if version not in feeds_versions: + update_file = await get_update_file(session, version) + install_file = await get_feeds_file(session, version) + await get_latest_update_file(session, update_file) + await get_latest_install_file(session, version, feeds_path, install_file) + else: + logging.info("Feeds are up to date.") + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(update_installer_files()) From b7c58e5d349bbfcaccb9ccb7a13d1f6798297422 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 09:37:06 -0600 Subject: [PATCH 07/25] add feeds updater to startup process --- network/__init__.py | 8 +++-- tools/web_testbench/app.py | 67 +++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/network/__init__.py b/network/__init__.py index 01ec4168..85bad28f 100644 --- a/network/__init__.py +++ b/network/__init__.py @@ -145,12 +145,16 @@ class MinerNetwork: return await ping_miner(ip) -async def ping_miner(ip: ipaddress.ip_address, port=4028) -> None or ipaddress.ip_address: +async def ping_miner( + ip: ipaddress.ip_address, port=4028 +) -> None or ipaddress.ip_address: for i in range(PING_RETRIES): connection_fut = asyncio.open_connection(str(ip), port) try: # get the read and write streams from the connection - reader, writer = await asyncio.wait_for(connection_fut, timeout=PING_TIMEOUT) + reader, writer = await asyncio.wait_for( + connection_fut, timeout=PING_TIMEOUT + ) # immediately close connection, we know connection happened writer.close() # make sure the writer is closed diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 25168c8f..8290da66 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -8,34 +8,35 @@ import os from fastapi.templating import Jinja2Templates from tools.web_testbench import miner_network +from tools.web_testbench.feeds import update_installer_files app = FastAPI() -app.mount("/public", StaticFiles( - directory=os.path.join(os.path.dirname(__file__), "public")), name="public") +app.mount( + "/public", + StaticFiles(directory=os.path.join(os.path.dirname(__file__), "public")), + name="public", +) templates = Jinja2Templates( - directory=os.path.join(os.path.dirname(__file__), "templates")) + directory=os.path.join(os.path.dirname(__file__), "templates") +) miner_data = { - 'IP': '192.168.1.10', - 'Light': 'show', - 'Fans': { - 'fan_0': {'RPM': 4620}, - 'fan_1': {'RPM': 4560}, - 'fan_2': {'RPM': 0}, - 'fan_3': {'RPM': 0} + "IP": "192.168.1.10", + "Light": "show", + "Fans": { + "fan_0": {"RPM": 4620}, + "fan_1": {"RPM": 4560}, + "fan_2": {"RPM": 0}, + "fan_3": {"RPM": 0}, }, - 'HR': { - 'board_6': {'HR': 4.85}, - 'board_7': {'HR': 0.0}, - 'board_8': {'HR': 0.81} + "HR": {"board_6": {"HR": 4.85}, "board_7": {"HR": 0.0}, "board_8": {"HR": 0.81}}, + "Temps": { + "board_6": {"Board": 85.6875, "Chip": 93.0}, + "board_7": {"Board": 0.0, "Chip": 0.0}, + "board_8": {"Board": 0.0, "Chip": 0.0}, }, - 'Temps': { - 'board_6': {'Board': 85.6875, 'Chip': 93.0}, - 'board_7': {'Board': 0.0, 'Chip': 0.0}, - 'board_8': {'Board': 0.0, 'Chip': 0.0} - } } @@ -45,15 +46,14 @@ class ConnectionManager: def __new__(cls): if not cls._instance: - cls._instance = super( - ConnectionManager, - cls - ).__new__(cls) + cls._instance = super(ConnectionManager, cls).__new__(cls) return cls._instance async def connect(self, websocket: WebSocket): await websocket.accept() - await websocket.send_json({"miners": [str(miner) for miner in miner_network.hosts()]}) + await websocket.send_json( + {"miners": [str(miner) for miner in miner_network.hosts()]} + ) ConnectionManager._connections.append(websocket) def disconnect(self, websocket: WebSocket): @@ -80,22 +80,31 @@ async def ws(websocket: WebSocket): ConnectionManager().disconnect(websocket) - @app.get("/") def dashboard(request: Request): - return templates.TemplateResponse("index.html", { - "request": request, - }) + return templates.TemplateResponse( + "index.html", + { + "request": request, + }, + ) + + +@app.on_event("startup") +async def update_installer(): + await update_installer_files() @app.on_event("startup") def start_monitor(): asyncio.create_task(monitor()) + async def monitor(): while True: await ConnectionManager().broadcast_json(miner_data) await asyncio.sleep(5) -if __name__ == '__main__': + +if __name__ == "__main__": uvicorn.run("app:app", host="0.0.0.0", port=80) From c93051022679ff580e8446a2f516a4997c7cb28f Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 09:43:43 -0600 Subject: [PATCH 08/25] added auto port finding to both web apps --- tools/web_monitor/templates/index.html | 2 +- tools/web_monitor/templates/miner.html | 2 +- tools/web_monitor/templates/scan.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/web_monitor/templates/index.html b/tools/web_monitor/templates/index.html index c7f99234..8ce2e32c 100644 --- a/tools/web_monitor/templates/index.html +++ b/tools/web_monitor/templates/index.html @@ -10,7 +10,7 @@ From f5a41f7b136c0d67f7391789c59e6ca5568e42e4 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 11:08:52 -0600 Subject: [PATCH 12/25] added output when running install process --- tools/web_testbench/__init__.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tools/web_testbench/__init__.py b/tools/web_testbench/__init__.py index 1c1f777b..76eb2dac 100644 --- a/tools/web_testbench/__init__.py +++ b/tools/web_testbench/__init__.py @@ -33,6 +33,7 @@ class testbenchMiner: MinerFactory().miners.remove(self.host) async def wait_for_disconnect(self): + await self.add_to_output("Waiting for disconnect...") while await ping_miner(self.host): await asyncio.sleep(1) @@ -40,19 +41,25 @@ class testbenchMiner: if not await ping_miner(self.host): return await self.remove_from_cache() - miner = MinerFactory().get_miner(self.host) + miner = await MinerFactory().get_miner(self.host) + await self.add_to_output("Found miner: " + miner) if isinstance(miner, BOSMinerS9): + await self.add_to_output("Already running BraiinsOS, updating.") self.state = UPDATE return if await ping_miner(self.host, 22): + await self.add_to_output("Miner is unlocked, installing.") self.state = INSTALL return + await self.add_to_output("Miner needs unlock, unlocking.") self.state = UNLOCK async def install_unlock(self): if await self.ssh_unlock(): + await self.add_to_output("Unlocked miner, installing.") self.state = INSTALL return + await self.add_to_output("Failed to unlock miner, please pin reset.") self.state = START await self.wait_for_disconnect() @@ -76,12 +83,14 @@ class testbenchMiner: # get stdout of the install while True: stdout = await proc.stderr.readuntil(b"\r") + await self.add_to_output(stdout) if stdout == b"": break await proc.wait() while not await ping_miner(self.host): await asyncio.sleep(3) await asyncio.sleep(5) + await self.add_to_output("Install complete, configuring.") self.state = REFERRAL async def install_update(self): @@ -91,8 +100,10 @@ class testbenchMiner: await miner.send_file(UPDATE_FILE_S9, "/tmp/firmware.tar") await miner.send_ssh_command("sysupgrade /tmp/firmware.tar") except: + await self.add_to_output("Failed to update, restarting.") self.state = START return + await self.add_to_output("Update complete, configuring.") self.state = REFERRAL async def install_referral(self): @@ -106,17 +117,25 @@ class testbenchMiner: "opkg install /tmp/referral.ipk && /etc/init.d/bosminer restart" ) except: + await self.add_to_output( + "Failed to add referral and configure, restarting." + ) self.state = START return else: + await self.add_to_output( + "Failed to add referral and configure, restarting." + ) self.state = START return + await self.add_to_output("Configuration complete.") self.state = DONE async def get_web_data(self): miner = await MinerFactory().get_miner(self.host) if not isinstance(miner, BOSMinerS9): + await self.add_to_output("Miner type changed, restarting.") self.state = START return try: From 2d6891c6d245bbe940972254aefa2f3da406b4ff Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 11:34:21 -0600 Subject: [PATCH 13/25] added partial fault light functionality and fixed stdout output direction --- tools/web_testbench/__init__.py | 12 +++++++- tools/web_testbench/app.py | 4 ++- tools/web_testbench/templates/index.html | 36 ++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/tools/web_testbench/__init__.py b/tools/web_testbench/__init__.py index 76eb2dac..9a526bca 100644 --- a/tools/web_testbench/__init__.py +++ b/tools/web_testbench/__init__.py @@ -21,6 +21,14 @@ class testbenchMiner: def __init__(self, host: ip_address): self.host = host self.state = START + self.light = False + + async def fault_light(self): + miner = await MinerFactory().get_miner(self.host) + if self.light: + await miner.fault_light_off() + else: + await miner.fault_light_on() async def add_to_output(self, message): await ConnectionManager().broadcast_json( @@ -194,9 +202,11 @@ class testbenchMiner: return async def install_done(self): + await self.add_to_output("Waiting for disconnect...") while await ping_miner(self.host) and self.state == DONE: - print(await self.get_web_data()) + await ConnectionManager().broadcast_json(await self.get_web_data()) await asyncio.sleep(1) + self.state = START async def install_loop(self): while True: diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 21f18fc6..ef470728 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -101,12 +101,14 @@ def start_monitor(): async def monitor(): + i = 0 while True: await ConnectionManager().broadcast_json( - {"IP": "192.168.1.11", "text": "hello\n"} + {"IP": "192.168.1.11", "text": f"hello - {i}\n"} ) await asyncio.sleep(5) await ConnectionManager().broadcast_json(miner_data) + i += 1 if __name__ == "__main__": diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index 29910ccb..cfd56484 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -216,6 +216,28 @@ var ws = new WebSocket("ws://{{request.url.hostname}}:{% if request.url.port %}{ miner_graphs.append(row_fan) column.append(miner_graphs) + + // create light button container + var container_light = document.createElement('div'); + container_light.className = "form-check form-switch d-flex justify-content-evenly" + + // create light button + var light_switch = document.createElement('input'); + light_switch.type = "checkbox" + light_switch.id = miner + "-light" + light_switch.className = "form-check-input" + + // add a light label to the button + var label_light = document.createElement("label"); + label_light.setAttribute("for", "light_" + miner.IP); + label_light.innerHTML = "Light"; + + // add the button and label to the container + container_light.append(label_light) + container_light.append(light_switch) + + column.append(container_light) + container_all.append(column) var chart_hr = new Chart(hr_canvas, { @@ -321,7 +343,6 @@ var ws = new WebSocket("ws://{{request.url.hostname}}:{% if request.url.port %}{ var fan_2_rpm = data["Fans"]["fan_1"]["RPM"] var fan_2_title = document.getElementById(data["IP"] + "-fan_r"); fan_2_title.innerHTML = "Fan R: " + fan_2_rpm + " RPM"; - console.log(fan_2_rpm); if (fan_2_rpm == 0){ var secondary_col_2 = "rgba(97, 4, 4, 1)" } else { @@ -335,9 +356,18 @@ var ws = new WebSocket("ws://{{request.url.hostname}}:{% if request.url.port %}{ miner_graphs.hidden = true var miner_stdout = document.getElementById(data["IP"] + "-stdout_text") miner_stdout.hidden = false - miner_stdout.innerHTML += data["text"] - } + miner_stdout.innerHTML = data["text"] + miner_stdout.innerHTML + }; + if (data.hasOwnProperty("Light")) { + if (data["Light"] == "show") { + + } else { + + } + }; + } + From 4776dce03818bc0c68c4848b0a9f5368bc71571e Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 13:16:16 -0600 Subject: [PATCH 14/25] finished light functionality --- miners/__init__.py | 4 + miners/bosminer.py | 2 + tools/web_testbench/__init__.py | 10 +- tools/web_testbench/app.py | 25 +- tools/web_testbench/templates/index.html | 588 ++++++++++++----------- 5 files changed, 329 insertions(+), 300 deletions(-) diff --git a/miners/__init__.py b/miners/__init__.py index 01823cff..ae248053 100644 --- a/miners/__init__.py +++ b/miners/__init__.py @@ -20,6 +20,7 @@ class BaseMiner: self.api = api self.api_type = None self.model = None + self.light = None async def _get_ssh_connection(self) -> asyncssh.connect: """Create a new asyncssh connection""" @@ -56,6 +57,9 @@ class BaseMiner: conn = self._get_ssh_connection() await asyncssh.scp((conn, src), dest) + async def check_light(self): + return self.light + async def get_board_info(self): return None diff --git a/miners/bosminer.py b/miners/bosminer.py index 3809978c..072cf771 100644 --- a/miners/bosminer.py +++ b/miners/bosminer.py @@ -40,12 +40,14 @@ class BOSMiner(BaseMiner): async def fault_light_on(self) -> None: """Sends command to turn on fault light on the miner.""" logging.debug(f"{self}: Sending fault_light on command.") + self.light = True await self.send_ssh_command("miner fault_light on") logging.debug(f"{self}: fault_light on command completed.") async def fault_light_off(self) -> None: """Sends command to turn off fault light on the miner.""" logging.debug(f"{self}: Sending fault_light off command.") + self.light = False await self.send_ssh_command("miner fault_light off") logging.debug(f"{self}: fault_light off command completed.") diff --git a/tools/web_testbench/__init__.py b/tools/web_testbench/__init__.py index 9a526bca..6d36e0f8 100644 --- a/tools/web_testbench/__init__.py +++ b/tools/web_testbench/__init__.py @@ -17,18 +17,10 @@ CONFIG_FILE = os.path.join(os.path.dirname(__file__), "files", "config.toml") (START, UNLOCK, INSTALL, UPDATE, REFERRAL, DONE) = range(6) -class testbenchMiner: +class TestbenchMiner: def __init__(self, host: ip_address): self.host = host self.state = START - self.light = False - - async def fault_light(self): - miner = await MinerFactory().get_miner(self.host) - if self.light: - await miner.fault_light_off() - else: - await miner.fault_light_on() async def add_to_output(self, message): await ConnectionManager().broadcast_json( diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index ef470728..56f8942c 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -9,6 +9,7 @@ from fastapi.templating import Jinja2Templates from tools.web_testbench._network import miner_network from tools.web_testbench.feeds import update_installer_files +from miners.miner_factory import MinerFactory app = FastAPI() @@ -51,9 +52,18 @@ class ConnectionManager: async def connect(self, websocket: WebSocket): await websocket.accept() - await websocket.send_json( - {"miners": [str(miner) for miner in miner_network.hosts()]} - ) + miners = [] + for host in miner_network.hosts(): + if host in MinerFactory().miners.keys(): + miners.append( + { + "IP": str(host), + "Light_On": await MinerFactory().miners[host].get_light(), + } + ) + else: + miners.append({"IP": str(host), "Light_On": None}) + await websocket.send_json({"miners": miners}) ConnectionManager._connections.append(websocket) def disconnect(self, websocket: WebSocket): @@ -73,7 +83,14 @@ async def ws(websocket: WebSocket): await ConnectionManager().connect(websocket) try: while True: - data = await websocket.receive() + data = await websocket.receive_json() + if "IP" in data.keys(): + print(data) + miner = await MinerFactory().get_miner(data["IP"]) + if data["Data"] == "unlight": + miner.fault_light_off() + if data["Data"] == "light": + miner.fault_light_on() except WebSocketDisconnect: ConnectionManager().disconnect(websocket) except RuntimeError: diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index cfd56484..58451010 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -79,294 +79,308 @@ var options_fans = { var ws = new WebSocket("ws://{{request.url.hostname}}:{% if request.url.port %}{{request.url.port}}{% else %}80{% endif %}/ws"); - ws.onmessage = function(event) { - var data = JSON.parse(event.data) - if (data.hasOwnProperty("miners")) { - var container_all = document.getElementById('chart_container'); - container_all.innerHTML = "" - data["miners"].forEach(function(miner) { - // create column with ID being the IP for later use - var column = document.createElement('div'); - column.className = "col border border-dark p-3" - column.id = miner - - // create IP address header - var header = document.createElement('button'); - header.className = "text-center btn btn-primary w-100" - header.onclick = function(){window.open("http://" + miner, '_blank');} - header.innerHTML += miner - - column.append(header) - - // create install stdout - var row_text = document.createElement('div'); - row_text.className = "row p-3" - row_text.id = miner + "-stdout" - - // create text area for data - var text_area = document.createElement('textarea'); - text_area.id = miner + "-stdout_text" - text_area.rows = "15" - text_area.className = "form-control" - text_area.style = "font-size: 12px" - text_area.disabled = true - text_area.readonly = true - - row_text.append(text_area) - - column.append(row_text) - - - // create hr and temp canvas - var hr_canvas = document.createElement('canvas'); - hr_canvas.width = 125 - hr_canvas.height = 125 - hr_canvas.id = miner + "-hr" - - var temp_canvas = document.createElement('canvas'); - temp_canvas.width = 125 - temp_canvas.height = 125 - temp_canvas.id = miner + "-temp" - - // create fan 1 title - var fan_1_title = document.createElement('p'); - fan_1_title.innerHTML += "Fan L: 0 RPM"; - fan_1_title.className = "text-center" - fan_1_title.id = miner + "-fan_l" - - // create fan 2 title - var fan_2_title = document.createElement('p'); - fan_2_title.innerHTML += "Fan R: 0 RPM"; - fan_2_title.className = "text-center" - fan_2_title.id = miner + "-fan_r" - - // create fan 1 canvas - var fan_1_canvas = document.createElement('canvas'); - fan_1_canvas.width = 100 - fan_1_canvas.height = 100 - fan_1_canvas.id = miner + "-fan-1" - - // create fan 2 canvas - var fan_2_canvas = document.createElement('canvas'); - fan_2_canvas.width = 100 - fan_2_canvas.height = 100 - fan_2_canvas.id = miner + "-fan-2" - - - // create row for hr and temp data - var row_hr = document.createElement('div'); - row_hr.className = "row" - - // create row for titles of fans - var row_fan_title = document.createElement('div'); - row_fan_title.className = "row" - - // create row for fan graphs - var row_fan = document.createElement('div'); - row_fan.className = "row" - - // create hr container - var container_col_hr = document.createElement('div'); - container_col_hr.className = "col w-50 ps-0 pe-4" - - // create temp container - var container_col_temp = document.createElement('div'); - container_col_temp.className = "col w-50 ps-0 pe-4" - - // create fan title 1 container - var container_col_title_fan_1 = document.createElement('div'); - container_col_title_fan_1.className = "col" - - // create fan title 2 container - var container_col_title_fan_2 = document.createElement('div'); - container_col_title_fan_2.className = "col" - - // create fan 1 data container - var container_col_fan_1 = document.createElement('div'); - container_col_fan_1.className = "col w-50 ps-3 pe-1" - - // create fan 2 data container - var container_col_fan_2 = document.createElement('div'); - container_col_fan_2.className = "col w-50 ps-3 pe-1" - - // append canvases to the appropriate container columns - container_col_hr.append(hr_canvas) - container_col_temp.append(temp_canvas) - container_col_title_fan_1.append(fan_1_title) - container_col_title_fan_2.append(fan_2_title) - container_col_fan_1.append(fan_1_canvas) - container_col_fan_2.append(fan_2_canvas) - - // add container columns to the correct rows - row_hr.append(container_col_hr) - row_hr.append(container_col_temp) - row_fan_title.append(container_col_title_fan_1) - row_fan_title.append(container_col_title_fan_2) - row_fan.append(container_col_fan_1) - row_fan.append(container_col_fan_2) - - // create miner graph container - var miner_graphs = document.createElement('div'); - miner_graphs.id = miner + "-graphs" - miner_graphs.hidden = true - - // append the rows to the column - miner_graphs.append(row_hr) - miner_graphs.append(row_fan_title) - miner_graphs.append(row_fan) - column.append(miner_graphs) - - - // create light button container - var container_light = document.createElement('div'); - container_light.className = "form-check form-switch d-flex justify-content-evenly" - - // create light button - var light_switch = document.createElement('input'); - light_switch.type = "checkbox" - light_switch.id = miner + "-light" - light_switch.className = "form-check-input" - - // add a light label to the button - var label_light = document.createElement("label"); - label_light.setAttribute("for", "light_" + miner.IP); - label_light.innerHTML = "Light"; - - // add the button and label to the container - container_light.append(label_light) - container_light.append(light_switch) - - column.append(container_light) - - container_all.append(column) - - var chart_hr = new Chart(hr_canvas, { - type: "bar", - data: { - labels: ["Hashrate"], - datasets: [], - }, - options: options_hr - }); - - var chart_temp = new Chart(temp_canvas, { - type: "bar", - data: { - labels: ["Temps"], - datasets: [], - }, - options: options_temp, - }); - - var chart_fan_1 = new Chart(fan_1_canvas, { - type: "doughnut", - data: { - labels: ["Fan L"], - datasets: [ - { - data: [], - // add colors - backgroundColor: [ - "rgba(103, 0, 221, 1)", - "rgba(199, 199, 199, 1)" - ] - }, - ] - }, - options: options_fans - }); - - - // create the fan 2 chart - var chart_fan_2 = new Chart(fan_2_canvas, { - type: "doughnut", - data: { - labels: ["Fan R"], - datasets: [ - { - data: [], - backgroundColor: [ - "rgba(103, 0, 221, 1)", - "rgba(199, 199, 199, 1)" - ] - }, - ] - }, - options: options_fans - }); - - }); - } - else if (data.hasOwnProperty("HR")) { - var miner_stdout = document.getElementById(data["IP"] + "-stdout") - miner_stdout.hidden = true - var miner_graphs = document.getElementById(data["IP"] + "-graphs") - miner_graphs.hidden = false - var hr_graph = Chart.getChart(data["IP"] + "-hr") - var temp_graph = Chart.getChart(data["IP"] + "-temp") - var fan_1_graph = Chart.getChart(data["IP"] + "-fan-1") - var fan_2_graph = Chart.getChart(data["IP"] + "-fan-2") - - // update hr graph data and call the Update method - var hr_data = [] - hr_data.push({label: "Board 6", data: [data["HR"]["board_6"]["HR"]], backgroundColor: ["rgba(0, 19, 97, 1)"]}); - hr_data.push({label: "Board 7", data: [data["HR"]["board_7"]["HR"]], backgroundColor: ["rgba(0, 84, 219, 1)"]}); - hr_data.push({label: "Board 8", data: [data["HR"]["board_8"]["HR"]], backgroundColor: ["rgba(36, 180, 224, 1)"]}); - hr_graph.data.datasets = hr_data; - hr_graph.update(); - - // update temp graph data and call the Update method - var temp_data = [] - temp_data.push({label: "Board 6 Chips", data: [data["Temps"]["board_6"]["Chip"]], backgroundColor: ["rgba(6, 92, 39, 1)"]}); - temp_data.push({label: "Board 6", data: [data["Temps"]["board_6"]["Board"]], backgroundColor: ["rgba(255, 15, 58, 1)"]}); - temp_data.push({label: "Board 7 Chips", data: [data["Temps"]["board_7"]["Chip"]], backgroundColor: ["rgba(6, 92, 39, 1)"]}); - temp_data.push({label: "Board 7", data: [data["Temps"]["board_7"]["Board"]], backgroundColor: ["rgba(255, 15, 58, 1)"]}); - temp_data.push({label: "Board 8 Chips", data: [data["Temps"]["board_8"]["Chip"]], backgroundColor: ["rgba(6, 92, 39, 1)"]}); - temp_data.push({label: "Board 8", data: [data["Temps"]["board_8"]["Board"]], backgroundColor: ["rgba(255, 15, 58, 1)"]}); - temp_graph.data.datasets = temp_data; - temp_graph.update(); - - // update fan 1 graph data and call the Update method - var fan_1_rpm = data["Fans"]["fan_0"]["RPM"] - var fan_1_title = document.getElementById(data["IP"] + "-fan_l"); - fan_1_title.innerHTML = "Fan L: " + fan_1_rpm + " RPM"; - if (fan_1_rpm == 0){ - var secondary_col_1 = "rgba(97, 4, 4, 1)" - } else { - var secondary_col_1 = "rgba(199, 199, 199, 1)" - } - var fan_1_data = [{label: "Fan Speed", data: [fan_1_rpm, 6000-fan_1_rpm], backgroundColor: ["rgba(103, 0, 221, 1)", secondary_col_1]}] - fan_1_graph.data.datasets = fan_1_data; - fan_1_graph.update(); - - // update fan 2 graph data and call the Update method - var fan_2_rpm = data["Fans"]["fan_1"]["RPM"] - var fan_2_title = document.getElementById(data["IP"] + "-fan_r"); - fan_2_title.innerHTML = "Fan R: " + fan_2_rpm + " RPM"; - if (fan_2_rpm == 0){ - var secondary_col_2 = "rgba(97, 4, 4, 1)" - } else { - var secondary_col_2 = "rgba(199, 199, 199, 1)" - } - var fan_2_data = [{label: "Fan Speed", data: [fan_2_rpm, 6000-fan_2_rpm], backgroundColor: ["rgba(103, 0, 221, 1)", secondary_col_2]}] - fan_2_graph.data.datasets = fan_2_data; - fan_2_graph.update(); - } else { - var miner_graphs = document.getElementById(data["IP"] + "-graphs") - miner_graphs.hidden = true - var miner_stdout = document.getElementById(data["IP"] + "-stdout_text") - miner_stdout.hidden = false - miner_stdout.innerHTML = data["text"] + miner_stdout.innerHTML - }; - if (data.hasOwnProperty("Light")) { - if (data["Light"] == "show") { - - } else { - - } - }; - +function lightMiner(ip, checkbox) { + // if the checkbox is checked turn the light on, otherwise off + if (checkbox.checked){ + ws.send(JSON.stringify({"IP": ip, "Data": "light"})) + } else if (!(checkbox.check)) { + ws.send(JSON.stringify({"IP": ip, "Data": "unlight"})) } +}; +ws.onmessage = function(event) { + var data = JSON.parse(event.data) + if (data.hasOwnProperty("miners")) { + var container_all = document.getElementById('chart_container'); + container_all.innerHTML = "" + data["miners"].forEach(function(miner) { + // create column with ID being the IP for later use + var column = document.createElement('div'); + column.className = "col border border-dark p-3" + column.id = miner["IP"] + + // create IP address header + var header = document.createElement('button'); + header.className = "text-center btn btn-primary w-100" + header.onclick = function(){window.open("http://" + miner["IP"], '_blank');} + header.innerHTML += miner["IP"] + + column.append(header) + + // create install stdout + var row_text = document.createElement('div'); + row_text.className = "row p-3" + row_text.id = miner["IP"] + "-stdout" + + // create text area for data + var text_area = document.createElement('textarea'); + text_area.id = miner["IP"] + "-stdout_text" + text_area.rows = "15" + text_area.className = "form-control" + text_area.style = "font-size: 12px" + text_area.disabled = true + text_area.readonly = true + + row_text.append(text_area) + + column.append(row_text) + + + // create hr and temp canvas + var hr_canvas = document.createElement('canvas'); + hr_canvas.width = 125 + hr_canvas.height = 125 + hr_canvas.id = miner["IP"] + "-hr" + + var temp_canvas = document.createElement('canvas'); + temp_canvas.width = 125 + temp_canvas.height = 125 + temp_canvas.id = miner["IP"] + "-temp" + + // create fan 1 title + var fan_1_title = document.createElement('p'); + fan_1_title.innerHTML += "Fan L: 0 RPM"; + fan_1_title.className = "text-center" + fan_1_title.id = miner["IP"] + "-fan_l" + + // create fan 2 title + var fan_2_title = document.createElement('p'); + fan_2_title.innerHTML += "Fan R: 0 RPM"; + fan_2_title.className = "text-center" + fan_2_title.id = miner["IP"] + "-fan_r" + + // create fan 1 canvas + var fan_1_canvas = document.createElement('canvas'); + fan_1_canvas.width = 100 + fan_1_canvas.height = 100 + fan_1_canvas.id = miner["IP"] + "-fan-1" + + // create fan 2 canvas + var fan_2_canvas = document.createElement('canvas'); + fan_2_canvas.width = 100 + fan_2_canvas.height = 100 + fan_2_canvas.id = miner["IP"] + "-fan-2" + + + // create row for hr and temp data + var row_hr = document.createElement('div'); + row_hr.className = "row" + + // create row for titles of fans + var row_fan_title = document.createElement('div'); + row_fan_title.className = "row" + + // create row for fan graphs + var row_fan = document.createElement('div'); + row_fan.className = "row mb-4" + + // create hr container + var container_col_hr = document.createElement('div'); + container_col_hr.className = "col w-50 ps-0 pe-4" + + // create temp container + var container_col_temp = document.createElement('div'); + container_col_temp.className = "col w-50 ps-0 pe-4" + + // create fan title 1 container + var container_col_title_fan_1 = document.createElement('div'); + container_col_title_fan_1.className = "col" + + // create fan title 2 container + var container_col_title_fan_2 = document.createElement('div'); + container_col_title_fan_2.className = "col" + + // create fan 1 data container + var container_col_fan_1 = document.createElement('div'); + container_col_fan_1.className = "col w-50 ps-3 pe-1" + + // create fan 2 data container + var container_col_fan_2 = document.createElement('div'); + container_col_fan_2.className = "col w-50 ps-3 pe-1" + + // append canvases to the appropriate container columns + container_col_hr.append(hr_canvas) + container_col_temp.append(temp_canvas) + container_col_title_fan_1.append(fan_1_title) + container_col_title_fan_2.append(fan_2_title) + container_col_fan_1.append(fan_1_canvas) + container_col_fan_2.append(fan_2_canvas) + + // add container columns to the correct rows + row_hr.append(container_col_hr) + row_hr.append(container_col_temp) + row_fan_title.append(container_col_title_fan_1) + row_fan_title.append(container_col_title_fan_2) + row_fan.append(container_col_fan_1) + row_fan.append(container_col_fan_2) + + // create miner graph container + var miner_graphs = document.createElement('div'); + miner_graphs.id = miner["IP"] + "-graphs" + miner_graphs.hidden = true + + // append the rows to the column + miner_graphs.append(row_hr) + miner_graphs.append(row_fan_title) + miner_graphs.append(row_fan) + column.append(miner_graphs) + + + // create light button container + var container_light = document.createElement('div'); + container_light.className = "form-check form-switch d-flex justify-content-evenly" + container_light.id = miner["IP"] + "-light_container" + + // create light button + var light_switch = document.createElement('input'); + light_switch.type = "checkbox" + if (miner["Light_On"] == true) { + light_switch.checked = true + } + light_switch.id = miner["IP"] + "-light" + light_switch.className = "form-check-input" + light_switch.addEventListener("click", function(){lightMiner(miner, light_switch);}, false); + + + // add a light label to the button + var label_light = document.createElement("label"); + label_light.setAttribute("for", miner["IP"] + "-light"); + label_light.innerHTML = "Light"; + + // add the button and label to the container + container_light.append(light_switch) + container_light.append(label_light) + + column.append(container_light) + + container_all.append(column) + + var chart_hr = new Chart(hr_canvas, { + type: "bar", + data: { + labels: ["Hashrate"], + datasets: [], + }, + options: options_hr + }); + + var chart_temp = new Chart(temp_canvas, { + type: "bar", + data: { + labels: ["Temps"], + datasets: [], + }, + options: options_temp, + }); + + var chart_fan_1 = new Chart(fan_1_canvas, { + type: "doughnut", + data: { + labels: ["Fan L"], + datasets: [ + { + data: [], + // add colors + backgroundColor: [ + "rgba(103, 0, 221, 1)", + "rgba(199, 199, 199, 1)" + ] + }, + ] + }, + options: options_fans + }); + + + // create the fan 2 chart + var chart_fan_2 = new Chart(fan_2_canvas, { + type: "doughnut", + data: { + labels: ["Fan R"], + datasets: [ + { + data: [], + backgroundColor: [ + "rgba(103, 0, 221, 1)", + "rgba(199, 199, 199, 1)" + ] + }, + ] + }, + options: options_fans + }); + + }); + } + else if (data.hasOwnProperty("HR")) { + var miner_stdout = document.getElementById(data["IP"] + "-stdout") + miner_stdout.hidden = true + var miner_graphs = document.getElementById(data["IP"] + "-graphs") + miner_graphs.hidden = false + var hr_graph = Chart.getChart(data["IP"] + "-hr") + var temp_graph = Chart.getChart(data["IP"] + "-temp") + var fan_1_graph = Chart.getChart(data["IP"] + "-fan-1") + var fan_2_graph = Chart.getChart(data["IP"] + "-fan-2") + + // update hr graph data and call the Update method + var hr_data = [] + hr_data.push({label: "Board 6", data: [data["HR"]["board_6"]["HR"]], backgroundColor: ["rgba(0, 19, 97, 1)"]}); + hr_data.push({label: "Board 7", data: [data["HR"]["board_7"]["HR"]], backgroundColor: ["rgba(0, 84, 219, 1)"]}); + hr_data.push({label: "Board 8", data: [data["HR"]["board_8"]["HR"]], backgroundColor: ["rgba(36, 180, 224, 1)"]}); + hr_graph.data.datasets = hr_data; + hr_graph.update(); + + // update temp graph data and call the Update method + var temp_data = [] + temp_data.push({label: "Board 6 Chips", data: [data["Temps"]["board_6"]["Chip"]], backgroundColor: ["rgba(6, 92, 39, 1)"]}); + temp_data.push({label: "Board 6", data: [data["Temps"]["board_6"]["Board"]], backgroundColor: ["rgba(255, 15, 58, 1)"]}); + temp_data.push({label: "Board 7 Chips", data: [data["Temps"]["board_7"]["Chip"]], backgroundColor: ["rgba(6, 92, 39, 1)"]}); + temp_data.push({label: "Board 7", data: [data["Temps"]["board_7"]["Board"]], backgroundColor: ["rgba(255, 15, 58, 1)"]}); + temp_data.push({label: "Board 8 Chips", data: [data["Temps"]["board_8"]["Chip"]], backgroundColor: ["rgba(6, 92, 39, 1)"]}); + temp_data.push({label: "Board 8", data: [data["Temps"]["board_8"]["Board"]], backgroundColor: ["rgba(255, 15, 58, 1)"]}); + temp_graph.data.datasets = temp_data; + temp_graph.update(); + + // update fan 1 graph data and call the Update method + var fan_1_rpm = data["Fans"]["fan_0"]["RPM"] + var fan_1_title = document.getElementById(data["IP"] + "-fan_l"); + fan_1_title.innerHTML = "Fan L: " + fan_1_rpm + " RPM"; + if (fan_1_rpm == 0){ + var secondary_col_1 = "rgba(97, 4, 4, 1)" + } else { + var secondary_col_1 = "rgba(199, 199, 199, 1)" + } + var fan_1_data = [{label: "Fan Speed", data: [fan_1_rpm, 6000-fan_1_rpm], backgroundColor: ["rgba(103, 0, 221, 1)", secondary_col_1]}] + fan_1_graph.data.datasets = fan_1_data; + fan_1_graph.update(); + + // update fan 2 graph data and call the Update method + var fan_2_rpm = data["Fans"]["fan_1"]["RPM"] + var fan_2_title = document.getElementById(data["IP"] + "-fan_r"); + fan_2_title.innerHTML = "Fan R: " + fan_2_rpm + " RPM"; + if (fan_2_rpm == 0){ + var secondary_col_2 = "rgba(97, 4, 4, 1)" + } else { + var secondary_col_2 = "rgba(199, 199, 199, 1)" + } + var fan_2_data = [{label: "Fan Speed", data: [fan_2_rpm, 6000-fan_2_rpm], backgroundColor: ["rgba(103, 0, 221, 1)", secondary_col_2]}] + fan_2_graph.data.datasets = fan_2_data; + fan_2_graph.update(); + } else { + var miner_graphs = document.getElementById(data["IP"] + "-graphs") + miner_graphs.hidden = true + var miner_stdout = document.getElementById(data["IP"] + "-stdout_text") + miner_stdout.hidden = false + miner_stdout.innerHTML = data["text"] + miner_stdout.innerHTML + }; + if (data.hasOwnProperty("Light")) { + light_box = document.getElementById(data["IP"] + "-light_container") + if (data["Light"] == "show") { + light_box.hidden = false + } else { + light_box.hidden = true + } + }; +} From 3a560472e6b6ab879581523f00d60c5f6274a711 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 14:40:31 -0600 Subject: [PATCH 15/25] finished miner install to be tested --- tools/web_testbench/__init__.py | 216 ---------------------------- tools/web_testbench/_miners.py | 218 +++++++++++++++++++++++++++++ tools/web_testbench/app.py | 76 ++-------- tools/web_testbench/connections.py | 41 ++++++ 4 files changed, 268 insertions(+), 283 deletions(-) create mode 100644 tools/web_testbench/_miners.py create mode 100644 tools/web_testbench/connections.py diff --git a/tools/web_testbench/__init__.py b/tools/web_testbench/__init__.py index 6d36e0f8..e69de29b 100644 --- a/tools/web_testbench/__init__.py +++ b/tools/web_testbench/__init__.py @@ -1,216 +0,0 @@ -from ipaddress import ip_address -import asyncio -import os - -from network import ping_miner -from miners.miner_factory import MinerFactory -from miners.antminer.S9.bosminer import BOSMinerS9 -from tools.web_testbench._network import miner_network -from tools.web_testbench.app import ConnectionManager - -REFERRAL_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "referral.ipk") -UPDATE_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "update.tar") -CONFIG_FILE = os.path.join(os.path.dirname(__file__), "files", "config.toml") - - -# static states -(START, UNLOCK, INSTALL, UPDATE, REFERRAL, DONE) = range(6) - - -class TestbenchMiner: - def __init__(self, host: ip_address): - self.host = host - self.state = START - - async def add_to_output(self, message): - await ConnectionManager().broadcast_json( - {"IP": self.host, "text": str(message)} - ) - return - - async def remove_from_cache(self): - if self.host in MinerFactory().miners.keys(): - MinerFactory().miners.remove(self.host) - - async def wait_for_disconnect(self): - await self.add_to_output("Waiting for disconnect...") - while await ping_miner(self.host): - await asyncio.sleep(1) - - async def install_start(self): - if not await ping_miner(self.host): - return - await self.remove_from_cache() - miner = await MinerFactory().get_miner(self.host) - await self.add_to_output("Found miner: " + miner) - if isinstance(miner, BOSMinerS9): - await self.add_to_output("Already running BraiinsOS, updating.") - self.state = UPDATE - return - if await ping_miner(self.host, 22): - await self.add_to_output("Miner is unlocked, installing.") - self.state = INSTALL - return - await self.add_to_output("Miner needs unlock, unlocking.") - self.state = UNLOCK - - async def install_unlock(self): - if await self.ssh_unlock(): - await self.add_to_output("Unlocked miner, installing.") - self.state = INSTALL - return - await self.add_to_output("Failed to unlock miner, please pin reset.") - self.state = START - await self.wait_for_disconnect() - - async def ssh_unlock(self): - proc = await asyncio.create_subprocess_shell( - f'{os.path.join(os.path.dirname(__file__), "files", "asicseer_installer.exe")} -p -f {str(self.host)} root', - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await proc.communicate() - if str(stdout).find("webUI") != -1: - return False - return True - - async def do_install(self): - proc = await asyncio.create_subprocess_shell( - f'{os.path.join(os.path.dirname(__file__), "files", "bos-toolbox", "bos-toolbox.bat")} install {str(self.host)} --no-keep-pools --psu-power-limit 900 --no-nand-backup --feeds-url file:./feeds/', - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - # get stdout of the install - while True: - stdout = await proc.stderr.readuntil(b"\r") - await self.add_to_output(stdout) - if stdout == b"": - break - await proc.wait() - while not await ping_miner(self.host): - await asyncio.sleep(3) - await asyncio.sleep(5) - await self.add_to_output("Install complete, configuring.") - self.state = REFERRAL - - async def install_update(self): - await self.remove_from_cache() - miner = await MinerFactory().get_miner(self.host) - try: - await miner.send_file(UPDATE_FILE_S9, "/tmp/firmware.tar") - await miner.send_ssh_command("sysupgrade /tmp/firmware.tar") - except: - await self.add_to_output("Failed to update, restarting.") - self.state = START - return - await self.add_to_output("Update complete, configuring.") - self.state = REFERRAL - - async def install_referral(self): - miner = await MinerFactory().get_miner(self.host) - if os.path.exists(REFERRAL_FILE_S9): - try: - await miner.send_file(REFERRAL_FILE_S9, "/tmp/referral.ipk") - await miner.send_file(CONFIG_FILE, "/etc/bosminer.toml") - - await miner.send_ssh_command( - "opkg install /tmp/referral.ipk && /etc/init.d/bosminer restart" - ) - except: - await self.add_to_output( - "Failed to add referral and configure, restarting." - ) - self.state = START - return - else: - await self.add_to_output( - "Failed to add referral and configure, restarting." - ) - self.state = START - return - await self.add_to_output("Configuration complete.") - self.state = DONE - - async def get_web_data(self): - miner = await MinerFactory().get_miner(self.host) - - if not isinstance(miner, BOSMinerS9): - await self.add_to_output("Miner type changed, restarting.") - self.state = START - return - try: - all_data = await miner.api.multicommand("devs", "temps", "fans") - - devs_raw = all_data["devs"][0] - temps_raw = all_data["temps"][0] - fans_raw = all_data["fans"][0] - - # parse temperature data - temps_data = {} - for board in range(len(temps_raw["TEMPS"])): - temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"] = {} - temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"][ - "Board" - ] = temps_raw["TEMPS"][board]["Board"] - temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"][ - "Chip" - ] = temps_raw["TEMPS"][board]["Chip"] - - # parse individual board and chip temperature data - for board in temps_data.keys(): - if "Board" not in temps_data[board].keys(): - temps_data[board]["Board"] = 0 - if "Chip" not in temps_data[board].keys(): - temps_data[board]["Chip"] = 0 - - # parse hashrate data - hr_data = {} - for board in range(len(devs_raw["DEVS"])): - hr_data[f"board_{devs_raw['DEVS'][board]['ID']}"] = {} - hr_data[f"board_{devs_raw['DEVS'][board]['ID']}"]["HR"] = round( - devs_raw["DEVS"][board]["MHS 5s"] / 1000000, 2 - ) - - # parse fan data - fans_data = {} - for fan in range(len(fans_raw["FANS"])): - fans_data[f"fan_{fans_raw['FANS'][fan]['ID']}"] = {} - fans_data[f"fan_{fans_raw['FANS'][fan]['ID']}"]["RPM"] = fans_raw[ - "FANS" - ][fan]["RPM"] - - # set the miner data - miner_data = { - "IP": self.host, - "Light": "show", - "Fans": fans_data, - "HR": hr_data, - "Temps": temps_data, - } - - # return stats - return miner_data - except: - return - - async def install_done(self): - await self.add_to_output("Waiting for disconnect...") - while await ping_miner(self.host) and self.state == DONE: - await ConnectionManager().broadcast_json(await self.get_web_data()) - await asyncio.sleep(1) - self.state = START - - async def install_loop(self): - while True: - if self.state == START: - await self.install_start() - if self.state == UNLOCK: - await self.install_unlock() - if self.state == INSTALL: - await self.do_install() - if self.state == UPDATE: - await self.install_update() - if self.state == REFERRAL: - await self.install_referral() - if self.state == DONE: - await self.install_done() diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py new file mode 100644 index 00000000..2f666640 --- /dev/null +++ b/tools/web_testbench/_miners.py @@ -0,0 +1,218 @@ +from ipaddress import ip_address +import asyncio +import os +import datetime + +from network import ping_miner +from miners.miner_factory import MinerFactory +from miners.antminer.S9.bosminer import BOSMinerS9 +from tools.web_testbench.connections import ConnectionManager + +REFERRAL_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "referral.ipk") +UPDATE_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "update.tar") +CONFIG_FILE = os.path.join(os.path.dirname(__file__), "files", "config.toml") + + +# static states +(START, UNLOCK, INSTALL, UPDATE, REFERRAL, DONE) = range(6) + + +class TestbenchMiner: + def __init__(self, host: ip_address): + self.host = host + self.state = START + + async def add_to_output(self, message): + print(datetime.datetime.now()) + await ConnectionManager().broadcast_json( + {"IP": str(self.host), "text": str(message) + "\n"} + ) + return + + async def remove_from_cache(self): + if self.host in MinerFactory().miners.keys(): + MinerFactory().miners.remove(self.host) + + async def wait_for_disconnect(self): + await self.add_to_output("Waiting for disconnect...") + while await ping_miner(self.host): + await asyncio.sleep(1) + + async def install_start(self): + if not await ping_miner(self.host): + await self.add_to_output("Waiting for miner connection...") + return + await self.remove_from_cache() + miner = await MinerFactory().get_miner(self.host) + await self.add_to_output("Found miner: " + miner) + if isinstance(miner, BOSMinerS9): + await self.add_to_output("Already running BraiinsOS, updating.") + self.state = UPDATE + return + if await ping_miner(self.host, 22): + await self.add_to_output("Miner is unlocked, installing.") + self.state = INSTALL + return + await self.add_to_output("Miner needs unlock, unlocking.") + self.state = UNLOCK + + async def install_unlock(self): + if await self.ssh_unlock(): + await self.add_to_output("Unlocked miner, installing.") + self.state = INSTALL + return + await self.add_to_output("Failed to unlock miner, please pin reset.") + self.state = START + await self.wait_for_disconnect() + + async def ssh_unlock(self): + proc = await asyncio.create_subprocess_shell( + f'{os.path.join(os.path.dirname(__file__), "files", "asicseer_installer.exe")} -p -f {str(self.host)} root', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + if str(stdout).find("webUI") != -1: + return False + return True + + async def do_install(self): + proc = await asyncio.create_subprocess_shell( + f'{os.path.join(os.path.dirname(__file__), "files", "bos-toolbox", "bos-toolbox.bat")} install {str(self.host)} --no-keep-pools --psu-power-limit 900 --no-nand-backup --feeds-url file:./feeds/', + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + # get stdout of the install + while True: + stdout = await proc.stderr.readuntil(b"\r") + await self.add_to_output(stdout) + if stdout == b"": + break + await proc.wait() + while not await ping_miner(self.host): + await asyncio.sleep(3) + await asyncio.sleep(5) + await self.add_to_output("Install complete, configuring.") + self.state = REFERRAL + + async def install_update(self): + await self.remove_from_cache() + miner = await MinerFactory().get_miner(self.host) + try: + await miner.send_file(UPDATE_FILE_S9, "/tmp/firmware.tar") + await miner.send_ssh_command("sysupgrade /tmp/firmware.tar") + except: + await self.add_to_output("Failed to update, restarting.") + self.state = START + return + await self.add_to_output("Update complete, configuring.") + self.state = REFERRAL + + async def install_referral(self): + miner = await MinerFactory().get_miner(self.host) + if os.path.exists(REFERRAL_FILE_S9): + try: + await miner.send_file(REFERRAL_FILE_S9, "/tmp/referral.ipk") + await miner.send_file(CONFIG_FILE, "/etc/bosminer.toml") + + await miner.send_ssh_command( + "opkg install /tmp/referral.ipk && /etc/init.d/bosminer restart" + ) + except: + await self.add_to_output( + "Failed to add referral and configure, restarting." + ) + self.state = START + return + else: + await self.add_to_output( + "Failed to add referral and configure, restarting." + ) + self.state = START + return + await self.add_to_output("Configuration complete.") + self.state = DONE + + async def get_web_data(self): + miner = await MinerFactory().get_miner(self.host) + + if not isinstance(miner, BOSMinerS9): + await self.add_to_output("Miner type changed, restarting.") + self.state = START + return + try: + all_data = await miner.api.multicommand("devs", "temps", "fans") + + devs_raw = all_data["devs"][0] + temps_raw = all_data["temps"][0] + fans_raw = all_data["fans"][0] + + # parse temperature data + temps_data = {} + for board in range(len(temps_raw["TEMPS"])): + temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"] = {} + temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"][ + "Board" + ] = temps_raw["TEMPS"][board]["Board"] + temps_data[f"board_{temps_raw['TEMPS'][board]['ID']}"][ + "Chip" + ] = temps_raw["TEMPS"][board]["Chip"] + + # parse individual board and chip temperature data + for board in temps_data.keys(): + if "Board" not in temps_data[board].keys(): + temps_data[board]["Board"] = 0 + if "Chip" not in temps_data[board].keys(): + temps_data[board]["Chip"] = 0 + + # parse hashrate data + hr_data = {} + for board in range(len(devs_raw["DEVS"])): + hr_data[f"board_{devs_raw['DEVS'][board]['ID']}"] = {} + hr_data[f"board_{devs_raw['DEVS'][board]['ID']}"]["HR"] = round( + devs_raw["DEVS"][board]["MHS 5s"] / 1000000, 2 + ) + + # parse fan data + fans_data = {} + for fan in range(len(fans_raw["FANS"])): + fans_data[f"fan_{fans_raw['FANS'][fan]['ID']}"] = {} + fans_data[f"fan_{fans_raw['FANS'][fan]['ID']}"]["RPM"] = fans_raw[ + "FANS" + ][fan]["RPM"] + + # set the miner data + miner_data = { + "IP": self.host, + "Light": "show", + "Fans": fans_data, + "HR": hr_data, + "Temps": temps_data, + } + + # return stats + return miner_data + except: + return + + async def install_done(self): + await self.add_to_output("Waiting for disconnect...") + while await ping_miner(self.host) and self.state == DONE: + await ConnectionManager().broadcast_json(await self.get_web_data()) + await asyncio.sleep(1) + self.state = START + + async def install_loop(self): + while True: + if self.state == START: + await self.install_start() + if self.state == UNLOCK: + await self.install_unlock() + if self.state == INSTALL: + await self.do_install() + if self.state == UPDATE: + await self.install_update() + if self.state == REFERRAL: + await self.install_referral() + if self.state == DONE: + await self.install_done() diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 56f8942c..5a51ada9 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -7,9 +7,11 @@ import uvicorn import os from fastapi.templating import Jinja2Templates -from tools.web_testbench._network import miner_network from tools.web_testbench.feeds import update_installer_files from miners.miner_factory import MinerFactory +from tools.web_testbench.connections import ConnectionManager +from tools.web_testbench._miners import TestbenchMiner +from tools.web_testbench._network import miner_network app = FastAPI() @@ -23,60 +25,6 @@ templates = Jinja2Templates( directory=os.path.join(os.path.dirname(__file__), "templates") ) -miner_data = { - "IP": "192.168.1.10", - "Light": "show", - "Fans": { - "fan_0": {"RPM": 4620}, - "fan_1": {"RPM": 4560}, - "fan_2": {"RPM": 0}, - "fan_3": {"RPM": 0}, - }, - "HR": {"board_6": {"HR": 4.85}, "board_7": {"HR": 0.0}, "board_8": {"HR": 0.81}}, - "Temps": { - "board_6": {"Board": 85.6875, "Chip": 93.0}, - "board_7": {"Board": 0.0, "Chip": 0.0}, - "board_8": {"Board": 0.0, "Chip": 0.0}, - }, -} - - -class ConnectionManager: - _instance = None - _connections = [] - - def __new__(cls): - if not cls._instance: - cls._instance = super(ConnectionManager, cls).__new__(cls) - return cls._instance - - async def connect(self, websocket: WebSocket): - await websocket.accept() - miners = [] - for host in miner_network.hosts(): - if host in MinerFactory().miners.keys(): - miners.append( - { - "IP": str(host), - "Light_On": await MinerFactory().miners[host].get_light(), - } - ) - else: - miners.append({"IP": str(host), "Light_On": None}) - await websocket.send_json({"miners": miners}) - ConnectionManager._connections.append(websocket) - - def disconnect(self, websocket: WebSocket): - print("Disconnected") - ConnectionManager._connections.remove(websocket) - - async def broadcast_json(self, data: dict): - for connection in ConnectionManager._connections: - try: - await connection.send_json(data) - except: - self.disconnect(connection) - @app.websocket("/ws") async def ws(websocket: WebSocket): @@ -85,7 +33,6 @@ async def ws(websocket: WebSocket): while True: data = await websocket.receive_json() if "IP" in data.keys(): - print(data) miner = await MinerFactory().get_miner(data["IP"]) if data["Data"] == "unlight": miner.fault_light_off() @@ -113,19 +60,14 @@ async def update_installer(): @app.on_event("startup") -def start_monitor(): - asyncio.create_task(monitor()) +def start_install(): + asyncio.create_task(install()) -async def monitor(): - i = 0 - while True: - await ConnectionManager().broadcast_json( - {"IP": "192.168.1.11", "text": f"hello - {i}\n"} - ) - await asyncio.sleep(5) - await ConnectionManager().broadcast_json(miner_data) - i += 1 +async def install(): + for host in miner_network.hosts(): + miner = TestbenchMiner(host) + asyncio.create_task(miner.install_loop()) if __name__ == "__main__": diff --git a/tools/web_testbench/connections.py b/tools/web_testbench/connections.py new file mode 100644 index 00000000..2ec8173c --- /dev/null +++ b/tools/web_testbench/connections.py @@ -0,0 +1,41 @@ +from fastapi import WebSocket + +from miners.miner_factory import MinerFactory +from tools.web_testbench._network import miner_network + + +class ConnectionManager: + _instance = None + _connections = [] + + def __new__(cls): + if not cls._instance: + cls._instance = super(ConnectionManager, cls).__new__(cls) + return cls._instance + + async def connect(self, websocket: WebSocket): + await websocket.accept() + miners = [] + for host in miner_network.hosts(): + if host in MinerFactory().miners.keys(): + miners.append( + { + "IP": str(host), + "Light_On": await MinerFactory().miners[host].get_light(), + } + ) + else: + miners.append({"IP": str(host), "Light_On": None}) + await websocket.send_json({"miners": miners}) + ConnectionManager._connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + print("Disconnected") + ConnectionManager._connections.remove(websocket) + + async def broadcast_json(self, data: dict): + for connection in ConnectionManager._connections: + try: + await connection.send_json(data) + except Exception as e: + self.disconnect(connection) From eb5a00b706945f0d0bfe1944f9f91032f17f9dcc Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 18:17:23 -0600 Subject: [PATCH 16/25] fixed many remaining bugs in testbench webserver, should be ready for use. --- miners/__init__.py | 4 +- network/net_range.py | 6 ++- tools/web_testbench/_miners.py | 67 +++++++++++++++--------- tools/web_testbench/_network.py | 2 +- tools/web_testbench/app.py | 8 ++- tools/web_testbench/connections.py | 8 +-- tools/web_testbench/feeds.py | 27 ++++++---- tools/web_testbench/templates/index.html | 2 +- 8 files changed, 79 insertions(+), 45 deletions(-) diff --git a/miners/__init__.py b/miners/__init__.py index ae248053..16f85f93 100644 --- a/miners/__init__.py +++ b/miners/__init__.py @@ -54,8 +54,8 @@ class BaseMiner: raise e async def send_file(self, src, dest): - conn = self._get_ssh_connection() - await asyncssh.scp((conn, src), dest) + async with (await self._get_ssh_connection()) as conn: + await asyncssh.scp(src, (conn, dest)) async def check_light(self): return self.light diff --git a/network/net_range.py b/network/net_range.py index 8e260b5a..d62e997d 100644 --- a/network/net_range.py +++ b/network/net_range.py @@ -19,8 +19,12 @@ class MinerNetworkRange: end_ip = ipaddress.ip_address(end) networks = ipaddress.summarize_address_range(start_ip, end_ip) for network in networks: + self.host_ips.append(network.network_address) for host in network.hosts(): - self.host_ips.append(host) + if host not in self.host_ips: + self.host_ips.append(host) + if network.broadcast_address not in self.host_ips: + self.host_ips.append(network.broadcast_address) def hosts(self): for x in self.host_ips: diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index 2f666640..aa713027 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -1,18 +1,17 @@ from ipaddress import ip_address import asyncio import os -import datetime from network import ping_miner from miners.miner_factory import MinerFactory from miners.antminer.S9.bosminer import BOSMinerS9 from tools.web_testbench.connections import ConnectionManager +from tools.web_testbench.feeds import get_local_versions REFERRAL_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "referral.ipk") UPDATE_FILE_S9 = os.path.join(os.path.dirname(__file__), "files", "update.tar") CONFIG_FILE = os.path.join(os.path.dirname(__file__), "files", "config.toml") - # static states (START, UNLOCK, INSTALL, UPDATE, REFERRAL, DONE) = range(6) @@ -21,11 +20,20 @@ class TestbenchMiner: def __init__(self, host: ip_address): self.host = host self.state = START + self.latest_version = None + + async def get_bos_version(self): + miner = await MinerFactory().get_miner(self.host) + result = await miner.send_ssh_command("cat /etc/bos_version") + version_base = result.stdout + version_base = version_base.strip() + version_base = version_base.split("-") + version = version_base[-2] + return version async def add_to_output(self, message): - print(datetime.datetime.now()) await ConnectionManager().broadcast_json( - {"IP": str(self.host), "text": str(message) + "\n"} + {"IP": str(self.host), "text": str(message).replace("\r", "") + "\n"} ) return @@ -39,13 +47,17 @@ class TestbenchMiner: await asyncio.sleep(1) async def install_start(self): - if not await ping_miner(self.host): + if not await ping_miner(self.host, 80): await self.add_to_output("Waiting for miner connection...") return await self.remove_from_cache() miner = await MinerFactory().get_miner(self.host) - await self.add_to_output("Found miner: " + miner) + await self.add_to_output("Found miner: " + str(miner)) if isinstance(miner, BOSMinerS9): + if await self.get_bos_version() == self.latest_version: + await self.add_to_output("Already running the latest version of BraiinsOS, configuring.") + self.state = REFERRAL + return await self.add_to_output("Already running BraiinsOS, updating.") self.state = UPDATE return @@ -84,8 +96,11 @@ class TestbenchMiner: ) # get stdout of the install while True: - stdout = await proc.stderr.readuntil(b"\r") - await self.add_to_output(stdout) + try: + stdout = await proc.stderr.readuntil(b"\r") + except asyncio.exceptions.IncompleteReadError: + break + await self.add_to_output(stdout.decode("utf-8").strip()) if stdout == b"": break await proc.wait() @@ -96,40 +111,38 @@ class TestbenchMiner: self.state = REFERRAL async def install_update(self): + await self.add_to_output("Updating miner...") await self.remove_from_cache() miner = await MinerFactory().get_miner(self.host) try: await miner.send_file(UPDATE_FILE_S9, "/tmp/firmware.tar") await miner.send_ssh_command("sysupgrade /tmp/firmware.tar") - except: + except Exception as e: + print(e) await self.add_to_output("Failed to update, restarting.") self.state = START return + await asyncio.sleep(10) await self.add_to_output("Update complete, configuring.") self.state = REFERRAL async def install_referral(self): + while not await ping_miner(self.host): + await asyncio.sleep(1) miner = await MinerFactory().get_miner(self.host) - if os.path.exists(REFERRAL_FILE_S9): - try: - await miner.send_file(REFERRAL_FILE_S9, "/tmp/referral.ipk") - await miner.send_file(CONFIG_FILE, "/etc/bosminer.toml") - - await miner.send_ssh_command( - "opkg install /tmp/referral.ipk && /etc/init.d/bosminer restart" - ) - except: - await self.add_to_output( - "Failed to add referral and configure, restarting." - ) - self.state = START - return - else: + try: + await miner.send_file(REFERRAL_FILE_S9, "/tmp/referral.ipk") + await miner.send_file(CONFIG_FILE, "/etc/bosminer.toml") + await miner.send_ssh_command( + "opkg install /tmp/referral.ipk && /etc/init.d/bosminer restart" + ) + except Exception as e: await self.add_to_output( "Failed to add referral and configure, restarting." ) self.state = START return + await asyncio.sleep(5) await self.add_to_output("Configuration complete.") self.state = DONE @@ -183,7 +196,7 @@ class TestbenchMiner: # set the miner data miner_data = { - "IP": self.host, + "IP": str(self.host), "Light": "show", "Fans": fans_data, "HR": hr_data, @@ -198,11 +211,13 @@ class TestbenchMiner: async def install_done(self): await self.add_to_output("Waiting for disconnect...") while await ping_miner(self.host) and self.state == DONE: - await ConnectionManager().broadcast_json(await self.get_web_data()) + data = await self.get_web_data() + await ConnectionManager().broadcast_json(data) await asyncio.sleep(1) self.state = START async def install_loop(self): + self.latest_version = sorted(await get_local_versions(), reverse=True)[0] while True: if self.state == START: await self.install_start() diff --git a/tools/web_testbench/_network.py b/tools/web_testbench/_network.py index caa52f9f..d4f59f14 100644 --- a/tools/web_testbench/_network.py +++ b/tools/web_testbench/_network.py @@ -1,3 +1,3 @@ from network import MinerNetwork -miner_network = MinerNetwork("192.168.1.10-192.168.1.33").get_network() +miner_network = MinerNetwork("192.168.1.11-192.168.1.34").get_network() diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 5a51ada9..73fafabc 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -35,9 +35,13 @@ async def ws(websocket: WebSocket): if "IP" in data.keys(): miner = await MinerFactory().get_miner(data["IP"]) if data["Data"] == "unlight": - miner.fault_light_off() + if data["IP"] in ConnectionManager.lit_miners: + ConnectionManager.lit_miners.remove(data["IP"]) + await miner.fault_light_off() if data["Data"] == "light": - miner.fault_light_on() + if data["IP"] not in ConnectionManager().lit_miners: + ConnectionManager.lit_miners.append(data["IP"]) + await miner.fault_light_on() except WebSocketDisconnect: ConnectionManager().disconnect(websocket) except RuntimeError: diff --git a/tools/web_testbench/connections.py b/tools/web_testbench/connections.py index 2ec8173c..a89346a1 100644 --- a/tools/web_testbench/connections.py +++ b/tools/web_testbench/connections.py @@ -7,6 +7,7 @@ from tools.web_testbench._network import miner_network class ConnectionManager: _instance = None _connections = [] + lit_miners = [] def __new__(cls): if not cls._instance: @@ -16,16 +17,17 @@ class ConnectionManager: async def connect(self, websocket: WebSocket): await websocket.accept() miners = [] + print(ConnectionManager.lit_miners) for host in miner_network.hosts(): - if host in MinerFactory().miners.keys(): + if str(host) in ConnectionManager.lit_miners: miners.append( { "IP": str(host), - "Light_On": await MinerFactory().miners[host].get_light(), + "Light_On": True, } ) else: - miners.append({"IP": str(host), "Light_On": None}) + miners.append({"IP": str(host), "Light_On": False}) await websocket.send_json({"miners": miners}) ConnectionManager._connections.append(websocket) diff --git a/tools/web_testbench/feeds.py b/tools/web_testbench/feeds.py index ee532f9a..0427dde3 100644 --- a/tools/web_testbench/feeds.py +++ b/tools/web_testbench/feeds.py @@ -109,6 +109,23 @@ async def get_latest_install_file(session, version, feeds_path, install_file): async def update_installer_files(): + feeds_path = os.path.join( + os.path.dirname(__file__), "files", "bos-toolbox", "feeds" + ) + feeds_versions = await get_local_versions() + async with aiohttp.ClientSession() as session: + version = await get_latest_version(session) + + if version not in feeds_versions: + update_file = await get_update_file(session, version) + install_file = await get_feeds_file(session, version) + await get_latest_update_file(session, update_file) + await get_latest_install_file(session, version, feeds_path, install_file) + else: + logging.info("Feeds are up to date.") + + +async def get_local_versions(): feeds_versions = [] feeds_path = os.path.join( os.path.dirname(__file__), "files", "bos-toolbox", "feeds" @@ -127,16 +144,8 @@ async def update_installer_files(): ver = line.strip().split("\t")[0] feeds_versions.append(ver) - async with aiohttp.ClientSession() as session: - version = await get_latest_version(session) + return feeds_versions - if version not in feeds_versions: - update_file = await get_update_file(session, version) - install_file = await get_feeds_file(session, version) - await get_latest_update_file(session, update_file) - await get_latest_install_file(session, version, feeds_path, install_file) - else: - logging.info("Feeds are up to date.") if __name__ == "__main__": diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index 58451010..7788ec42 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -238,7 +238,7 @@ ws.onmessage = function(event) { } light_switch.id = miner["IP"] + "-light" light_switch.className = "form-check-input" - light_switch.addEventListener("click", function(){lightMiner(miner, light_switch);}, false); + light_switch.addEventListener("click", function(){lightMiner(miner["IP"], light_switch);}, false); // add a light label to the button From b5c455ffa47b1edd7ea83ef99942071b92a86a05 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 18:38:29 -0600 Subject: [PATCH 17/25] fixed more bugs --- tools/web_testbench/_miners.py | 1 + tools/web_testbench/app.py | 19 +++++++++++-------- tools/web_testbench/templates/index.html | 15 +++++++++------ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index aa713027..6d979f80 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -47,6 +47,7 @@ class TestbenchMiner: await asyncio.sleep(1) async def install_start(self): + await ConnectionManager().broadcast_json({"IP": str(self.host), "Light": "hide"}) if not await ping_miner(self.host, 80): await self.add_to_output("Waiting for miner connection...") return diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 73fafabc..4837ed41 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -34,14 +34,17 @@ async def ws(websocket: WebSocket): data = await websocket.receive_json() if "IP" in data.keys(): miner = await MinerFactory().get_miner(data["IP"]) - if data["Data"] == "unlight": - if data["IP"] in ConnectionManager.lit_miners: - ConnectionManager.lit_miners.remove(data["IP"]) - await miner.fault_light_off() - if data["Data"] == "light": - if data["IP"] not in ConnectionManager().lit_miners: - ConnectionManager.lit_miners.append(data["IP"]) - await miner.fault_light_on() + try: + if data["Data"] == "unlight": + if data["IP"] in ConnectionManager.lit_miners: + ConnectionManager.lit_miners.remove(data["IP"]) + await miner.fault_light_off() + if data["Data"] == "light": + if data["IP"] not in ConnectionManager().lit_miners: + ConnectionManager.lit_miners.append(data["IP"]) + await miner.fault_light_on() + except AttributeError: + await ConnectionManager().broadcast_json({"IP": data["IP"], "text": "Fault light command failed, miner is not running BraiinsOS."}) except WebSocketDisconnect: ConnectionManager().disconnect(websocket) except RuntimeError: diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index 7788ec42..9066fce0 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -227,7 +227,8 @@ ws.onmessage = function(event) { // create light button container var container_light = document.createElement('div'); - container_light.className = "form-check form-switch d-flex justify-content-evenly" + container_light.className = "form-check form-switch justify-content-evenly" + container_light.style = "display: none;" container_light.id = miner["IP"] + "-light_container" // create light button @@ -365,7 +366,7 @@ ws.onmessage = function(event) { var fan_2_data = [{label: "Fan Speed", data: [fan_2_rpm, 6000-fan_2_rpm], backgroundColor: ["rgba(103, 0, 221, 1)", secondary_col_2]}] fan_2_graph.data.datasets = fan_2_data; fan_2_graph.update(); - } else { + } else if (data.hasOwnProperty("text")) { var miner_graphs = document.getElementById(data["IP"] + "-graphs") miner_graphs.hidden = true var miner_stdout = document.getElementById(data["IP"] + "-stdout_text") @@ -374,12 +375,14 @@ ws.onmessage = function(event) { }; if (data.hasOwnProperty("Light")) { light_box = document.getElementById(data["IP"] + "-light_container") + console.log(data) if (data["Light"] == "show") { - light_box.hidden = false - } else { - light_box.hidden = true + console.log(light_box) + light_box.style = "display: flex;" + } else if (data["Light"] == "hide") { + light_box.style = "display: none;" } - }; + } } From 13f033440d7b471b8157138ac9217b8af77712b5 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Thu, 14 Apr 2022 18:43:36 -0600 Subject: [PATCH 18/25] added web testbench to main apps --- web_testbench.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 web_testbench.py diff --git a/web_testbench.py b/web_testbench.py new file mode 100644 index 00000000..f6f00bd4 --- /dev/null +++ b/web_testbench.py @@ -0,0 +1,9 @@ +from tools.web_testbench.app import app +import uvicorn + +def main(): + uvicorn.run("web_testbench:app", host="0.0.0.0", port=80) + + +if __name__ == "__main__": + main() \ No newline at end of file From 4f86dec560ca41a25cace312a47c0c5a4a7abfec Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 08:49:21 -0600 Subject: [PATCH 19/25] changed some printing to logging logs --- network/__init__.py | 1 - tools/web_testbench/_miners.py | 3 ++- tools/web_testbench/connections.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/network/__init__.py b/network/__init__.py index 85bad28f..553eff80 100644 --- a/network/__init__.py +++ b/network/__init__.py @@ -32,7 +32,6 @@ class MinerNetwork: return self.network if "-" in self.ip_addr: - print("getting network") self.network = MinerNetworkRange(self.ip_addr) else: # if there is no IP address passed, default to 192.168.1.0 diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index 6d979f80..7281d465 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -1,6 +1,7 @@ from ipaddress import ip_address import asyncio import os +import logging from network import ping_miner from miners.miner_factory import MinerFactory @@ -119,7 +120,7 @@ class TestbenchMiner: await miner.send_file(UPDATE_FILE_S9, "/tmp/firmware.tar") await miner.send_ssh_command("sysupgrade /tmp/firmware.tar") except Exception as e: - print(e) + logging.warning(f"{str(self.host)} Exception: {e}") await self.add_to_output("Failed to update, restarting.") self.state = START return diff --git a/tools/web_testbench/connections.py b/tools/web_testbench/connections.py index a89346a1..a5765f51 100644 --- a/tools/web_testbench/connections.py +++ b/tools/web_testbench/connections.py @@ -1,4 +1,5 @@ from fastapi import WebSocket +import logging from miners.miner_factory import MinerFactory from tools.web_testbench._network import miner_network @@ -17,7 +18,6 @@ class ConnectionManager: async def connect(self, websocket: WebSocket): await websocket.accept() miners = [] - print(ConnectionManager.lit_miners) for host in miner_network.hosts(): if str(host) in ConnectionManager.lit_miners: miners.append( @@ -32,7 +32,7 @@ class ConnectionManager: ConnectionManager._connections.append(websocket) def disconnect(self, websocket: WebSocket): - print("Disconnected") + logging.info("Disconnected") ConnectionManager._connections.remove(websocket) async def broadcast_json(self, data: dict): From 045e1ca6ba3dff417177d3b959f95d3dc74ce7bd Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 09:52:45 -0600 Subject: [PATCH 20/25] fixed some bugs with finishing the install --- settings/settings.toml | 2 +- tools/web_testbench/_miners.py | 21 ++++++++++++++++----- tools/web_testbench/templates/index.html | 9 +++++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/settings/settings.toml b/settings/settings.toml index 81f57ac3..2198aa33 100644 --- a/settings/settings.toml +++ b/settings/settings.toml @@ -1,6 +1,6 @@ get_version_retries = 3 ping_retries = 3 -ping_timeout = 5 +ping_timeout = 5 # Seconds scan_threads = 300 config_threads = 300 reboot_threads = 300 diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index 7281d465..a73daffb 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -57,7 +57,7 @@ class TestbenchMiner: await self.add_to_output("Found miner: " + str(miner)) if isinstance(miner, BOSMinerS9): if await self.get_bos_version() == self.latest_version: - await self.add_to_output("Already running the latest version of BraiinsOS, configuring.") + await self.add_to_output(f"Already running the latest version of BraiinsOS, {self.latest_version}, configuring.") self.state = REFERRAL return await self.add_to_output("Already running BraiinsOS, updating.") @@ -173,6 +173,11 @@ class TestbenchMiner: "Chip" ] = temps_raw["TEMPS"][board]["Chip"] + if len(temps_data.keys()) < 3: + for board in [6, 7, 8]: + if f"board_{board}" not in temps_data.keys(): + temps_data[f"board_{board}"] = {"Chip": 0, "Board": 0} + # parse individual board and chip temperature data for board in temps_data.keys(): if "Board" not in temps_data[board].keys(): @@ -212,11 +217,17 @@ class TestbenchMiner: async def install_done(self): await self.add_to_output("Waiting for disconnect...") - while await ping_miner(self.host) and self.state == DONE: - data = await self.get_web_data() - await ConnectionManager().broadcast_json(data) - await asyncio.sleep(1) + try: + while await ping_miner(self.host) and self.state == DONE: + data = await self.get_web_data() + await ConnectionManager().broadcast_json(data) + await asyncio.sleep(1) + except: + self.state = START + await self.add_to_output("Miner disconnected, waiting for new miner.") + return self.state = START + await self.add_to_output("Miner disconnected, waiting for new miner.") async def install_loop(self): self.latest_version = sorted(await get_local_versions(), reverse=True)[0] diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index 9066fce0..cef3b1cf 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -369,15 +369,16 @@ ws.onmessage = function(event) { } else if (data.hasOwnProperty("text")) { var miner_graphs = document.getElementById(data["IP"] + "-graphs") miner_graphs.hidden = true - var miner_stdout = document.getElementById(data["IP"] + "-stdout_text") + var miner_stdout = document.getElementById(data["IP"] + "-stdout") + var miner_stdout_text = document.getElementById(data["IP"] + "-stdout_text") miner_stdout.hidden = false - miner_stdout.innerHTML = data["text"] + miner_stdout.innerHTML + miner_stdout_text.innerHTML = data["text"] + miner_stdout_text.innerHTML + } else { + console.log(data) }; if (data.hasOwnProperty("Light")) { light_box = document.getElementById(data["IP"] + "-light_container") - console.log(data) if (data["Light"] == "show") { - console.log(light_box) light_box.style = "display: flex;" } else if (data["Light"] == "hide") { light_box.style = "display: none;" From cace399ed2f08cbf6bfdef24ab14980ef7b36e56 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 10:13:48 -0600 Subject: [PATCH 21/25] added fixing file exists bug --- tools/web_testbench/_miners.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index a73daffb..369146e8 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -90,7 +90,16 @@ class TestbenchMiner: return False return True + + async def fix_file_exists_bug(self): + miner = await MinerFactory().get_miner(self.host) + await miner.send_ssh_command( + "rm /lib/ld-musl-armhf.so.1; rm /usr/lib/openssh/sftp-server; rm /usr/sbin/fw_printenv" + ) + + async def do_install(self): + error = False proc = await asyncio.create_subprocess_shell( f'{os.path.join(os.path.dirname(__file__), "files", "bos-toolbox", "bos-toolbox.bat")} install {str(self.host)} --no-keep-pools --psu-power-limit 900 --no-nand-backup --feeds-url file:./feeds/', stdout=asyncio.subprocess.PIPE, @@ -102,13 +111,21 @@ class TestbenchMiner: stdout = await proc.stderr.readuntil(b"\r") except asyncio.exceptions.IncompleteReadError: break - await self.add_to_output(stdout.decode("utf-8").strip()) + stdout_data = stdout.decode("utf-8").strip() + if "ERROR:File" in stdout_data: + error = True + await self.add_to_output(stdout_data) if stdout == b"": break await proc.wait() while not await ping_miner(self.host): await asyncio.sleep(3) await asyncio.sleep(5) + if error: + await self.add_to_output("Encountered error, attempting to fix.") + await self.fix_file_exists_bug() + self.state = START + return await self.add_to_output("Install complete, configuring.") self.state = REFERRAL From 25e657729c904508190f43fbc58512f198b1c88a Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 10:24:53 -0600 Subject: [PATCH 22/25] reformatted files --- tools/web_testbench/_miners.py | 10 ++++++---- tools/web_testbench/app.py | 7 ++++++- tools/web_testbench/feeds.py | 1 - 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index 369146e8..d9419fd3 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -48,7 +48,9 @@ class TestbenchMiner: await asyncio.sleep(1) async def install_start(self): - await ConnectionManager().broadcast_json({"IP": str(self.host), "Light": "hide"}) + await ConnectionManager().broadcast_json( + {"IP": str(self.host), "Light": "hide"} + ) if not await ping_miner(self.host, 80): await self.add_to_output("Waiting for miner connection...") return @@ -57,7 +59,9 @@ class TestbenchMiner: await self.add_to_output("Found miner: " + str(miner)) if isinstance(miner, BOSMinerS9): if await self.get_bos_version() == self.latest_version: - await self.add_to_output(f"Already running the latest version of BraiinsOS, {self.latest_version}, configuring.") + await self.add_to_output( + f"Already running the latest version of BraiinsOS, {self.latest_version}, configuring." + ) self.state = REFERRAL return await self.add_to_output("Already running BraiinsOS, updating.") @@ -90,14 +94,12 @@ class TestbenchMiner: return False return True - async def fix_file_exists_bug(self): miner = await MinerFactory().get_miner(self.host) await miner.send_ssh_command( "rm /lib/ld-musl-armhf.so.1; rm /usr/lib/openssh/sftp-server; rm /usr/sbin/fw_printenv" ) - async def do_install(self): error = False proc = await asyncio.create_subprocess_shell( diff --git a/tools/web_testbench/app.py b/tools/web_testbench/app.py index 4837ed41..a523f18b 100644 --- a/tools/web_testbench/app.py +++ b/tools/web_testbench/app.py @@ -44,7 +44,12 @@ async def ws(websocket: WebSocket): ConnectionManager.lit_miners.append(data["IP"]) await miner.fault_light_on() except AttributeError: - await ConnectionManager().broadcast_json({"IP": data["IP"], "text": "Fault light command failed, miner is not running BraiinsOS."}) + await ConnectionManager().broadcast_json( + { + "IP": data["IP"], + "text": "Fault light command failed, miner is not running BraiinsOS.", + } + ) except WebSocketDisconnect: ConnectionManager().disconnect(websocket) except RuntimeError: diff --git a/tools/web_testbench/feeds.py b/tools/web_testbench/feeds.py index 0427dde3..c39de8d0 100644 --- a/tools/web_testbench/feeds.py +++ b/tools/web_testbench/feeds.py @@ -147,6 +147,5 @@ async def get_local_versions(): return feeds_versions - if __name__ == "__main__": asyncio.get_event_loop().run_until_complete(update_installer_files()) From 3b716a044b58903e5e6aa6dc8773b7204bcbb4d6 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 12:13:41 -0600 Subject: [PATCH 23/25] added online timer for testing --- tools/cfg_util/cfg_util_sg/func/miners.py | 4 +-- tools/web_testbench/_miners.py | 23 +++++++++++++--- tools/web_testbench/templates/index.html | 32 +++++++++++++++++++++-- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/tools/cfg_util/cfg_util_sg/func/miners.py b/tools/cfg_util/cfg_util_sg/func/miners.py index 7ff9bfad..4ed78680 100644 --- a/tools/cfg_util/cfg_util_sg/func/miners.py +++ b/tools/cfg_util/cfg_util_sg/func/miners.py @@ -196,8 +196,8 @@ async def restart_miners_backend(ips: list): progress_bar_len += 1 asyncio.create_task(update_prog_bar(progress_bar_len)) - reboot_miners_generator = reboot_generator(all_miners) - async for _rebooter in reboot_miners_generator: + restart_backend_gen = restart_backend_generator(all_miners) + async for _rebooter in restart_backend_gen: progress_bar_len += 1 asyncio.create_task(update_prog_bar(progress_bar_len)) await update_ui_with_data("status", "") diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index d9419fd3..c78a0e97 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -2,6 +2,7 @@ from ipaddress import ip_address import asyncio import os import logging +import datetime from network import ping_miner from miners.miner_factory import MinerFactory @@ -22,6 +23,7 @@ class TestbenchMiner: self.host = host self.state = START self.latest_version = None + self.start_time = None async def get_bos_version(self): miner = await MinerFactory().get_miner(self.host) @@ -32,10 +34,20 @@ class TestbenchMiner: version = version_base[-2] return version + def get_online_time(self): + online_time = "0:00:00" + if self.start_time: + online_time = str(datetime.datetime.now() - self.start_time) + return online_time + async def add_to_output(self, message): - await ConnectionManager().broadcast_json( - {"IP": str(self.host), "text": str(message).replace("\r", "") + "\n"} - ) + data = { + "IP": str(self.host), + "text": str(message).replace("\r", "") + "\n", + "online": self.get_online_time(), + } + + await ConnectionManager().broadcast_json(data) return async def remove_from_cache(self): @@ -48,8 +60,9 @@ class TestbenchMiner: await asyncio.sleep(1) async def install_start(self): + self.start_time = datetime.datetime.now() await ConnectionManager().broadcast_json( - {"IP": str(self.host), "Light": "hide"} + {"IP": str(self.host), "Light": "hide", "online": self.get_online_time()} ) if not await ping_miner(self.host, 80): await self.add_to_output("Waiting for miner connection...") @@ -227,6 +240,7 @@ class TestbenchMiner: "Fans": fans_data, "HR": hr_data, "Temps": temps_data, + "online": self.get_online_time(), } # return stats @@ -252,6 +266,7 @@ class TestbenchMiner: self.latest_version = sorted(await get_local_versions(), reverse=True)[0] while True: if self.state == START: + self.start_time = None await self.install_start() if self.state == UNLOCK: await self.install_unlock() diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index cef3b1cf..0cbe10df 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -89,6 +89,20 @@ function lightMiner(ip, checkbox) { }; ws.onmessage = function(event) { var data = JSON.parse(event.data) + if (data.hasOwnProperty("online")) { + timer = document.getElementById(data["IP"] + "-timer") + if (data["online"] == "0:00:00") { + if (timer.className.contains("btn-success")) { + timer.className.remove("btn-success") + timer.className += ("btn-secondary") + } + } else { + if (timer.className.contains("btn-secondary")) { + timer.className.remove("btn-secondary") + timer.className += ("btn-success") + } + } + }; if (data.hasOwnProperty("miners")) { var container_all = document.getElementById('chart_container'); container_all.innerHTML = "" @@ -98,13 +112,27 @@ ws.onmessage = function(event) { column.className = "col border border-dark p-3" column.id = miner["IP"] + // create button group + var button_group = document.createElement("div"); + button_group.className = "btn-group w-100" + // create IP address header var header = document.createElement('button'); - header.className = "text-center btn btn-primary w-100" + header.className = "text-center btn btn-primary" header.onclick = function(){window.open("http://" + miner["IP"], '_blank');} header.innerHTML += miner["IP"] - column.append(header) + // create online timer + var timer = document.createElement('button'); + timer.className = "text-center btn btn-secondary" + timer.disabled = true + timer.innerHTML = "0:00:00" + timer.id = miner["IP"] + "-timer" + + button_group.append(header) + button_group.append(timer) + + column.append(button_group) // create install stdout var row_text = document.createElement('div'); From 4468fe9fbb027c2c0d0f4d337dbb794db2448619 Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 12:29:55 -0600 Subject: [PATCH 24/25] finished adding timer --- tools/web_testbench/_miners.py | 10 ++++++---- tools/web_testbench/templates/index.html | 17 ++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index c78a0e97..fca51e26 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -37,7 +37,7 @@ class TestbenchMiner: def get_online_time(self): online_time = "0:00:00" if self.start_time: - online_time = str(datetime.datetime.now() - self.start_time) + online_time = str(datetime.datetime.now() - self.start_time).split(".")[0] return online_time async def add_to_output(self, message): @@ -60,13 +60,13 @@ class TestbenchMiner: await asyncio.sleep(1) async def install_start(self): + if not await ping_miner(self.host, 80): + await self.add_to_output("Waiting for miner connection...") + return self.start_time = datetime.datetime.now() await ConnectionManager().broadcast_json( {"IP": str(self.host), "Light": "hide", "online": self.get_online_time()} ) - if not await ping_miner(self.host, 80): - await self.add_to_output("Waiting for miner connection...") - return await self.remove_from_cache() miner = await MinerFactory().get_miner(self.host) await self.add_to_output("Found miner: " + str(miner)) @@ -258,9 +258,11 @@ class TestbenchMiner: except: self.state = START await self.add_to_output("Miner disconnected, waiting for new miner.") + self.start_time = None return self.state = START await self.add_to_output("Miner disconnected, waiting for new miner.") + self.start_time = None async def install_loop(self): self.latest_version = sorted(await get_local_versions(), reverse=True)[0] diff --git a/tools/web_testbench/templates/index.html b/tools/web_testbench/templates/index.html index 0cbe10df..b53144cc 100644 --- a/tools/web_testbench/templates/index.html +++ b/tools/web_testbench/templates/index.html @@ -92,16 +92,17 @@ ws.onmessage = function(event) { if (data.hasOwnProperty("online")) { timer = document.getElementById(data["IP"] + "-timer") if (data["online"] == "0:00:00") { - if (timer.className.contains("btn-success")) { - timer.className.remove("btn-success") - timer.className += ("btn-secondary") + if (timer.classList.contains("btn-success")) { + timer.classList.remove("btn-success") + timer.className += " btn-secondary" } } else { - if (timer.className.contains("btn-secondary")) { - timer.className.remove("btn-secondary") - timer.className += ("btn-success") + if (timer.classList.contains("btn-secondary")) { + timer.classList.remove("btn-secondary") + timer.className += " btn-success" } } + timer.innerHTML = data["online"] }; if (data.hasOwnProperty("miners")) { var container_all = document.getElementById('chart_container'); @@ -401,9 +402,7 @@ ws.onmessage = function(event) { var miner_stdout_text = document.getElementById(data["IP"] + "-stdout_text") miner_stdout.hidden = false miner_stdout_text.innerHTML = data["text"] + miner_stdout_text.innerHTML - } else { - console.log(data) - }; + } if (data.hasOwnProperty("Light")) { light_box = document.getElementById(data["IP"] + "-light_container") if (data["Light"] == "show") { From 4d58129eee9206456cfe1d3379badff4123aa40c Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Mon, 18 Apr 2022 13:12:08 -0600 Subject: [PATCH 25/25] fixed a bug with not hiding the light button --- tools/web_testbench/_miners.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/web_testbench/_miners.py b/tools/web_testbench/_miners.py index fca51e26..25d926f1 100644 --- a/tools/web_testbench/_miners.py +++ b/tools/web_testbench/_miners.py @@ -44,7 +44,8 @@ class TestbenchMiner: data = { "IP": str(self.host), "text": str(message).replace("\r", "") + "\n", - "online": self.get_online_time(), + "Light": "hide", + "online": self.get_online_time() } await ConnectionManager().broadcast_json(data)