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 + + +
+
+
+
+
+ +