diff --git a/main/http_server/axe-os/src/app/components/home/home.component.html b/main/http_server/axe-os/src/app/components/home/home.component.html index 29c8e957b..bfa57757e 100644 --- a/main/http_server/axe-os/src/app/components/home/home.component.html +++ b/main/http_server/axe-os/src/app/components/home/home.component.html @@ -30,7 +30,7 @@ Hashrate
- {{info.hashRate * 1000000000 | hashSuffix}} + {{info.hashRate | hashSuffix}} Not available - Power fault @@ -47,7 +47,7 @@
- Expected: {{info.expectedHashrate * 1000000000 | hashSuffix}} + Expected: {{info.expectedHashrate | hashSuffix}}
@@ -58,7 +58,7 @@ Efficiency
- {{info.power / (info.hashRate/1000) | number: '1.2-2'}} J/Th + {{info.power / (info.hashRate / 1000000000000) | number: '1.2-2'}} J/Th Not available - Power fault @@ -70,12 +70,12 @@ Average: - {{calculateEfficiencyAverage(hashrateData, powerData) | number: '1.2-2'}} J/Th + {{calculateEfficiencyAverage(hashrateData, powerData) | number: '1.2-2'}} J/Th
- Expected: {{info.power / (info.expectedHashrate/1000) | number: '1.2-2'}} J/Th + Expected: {{info.power / (info.expectedHashrate / 1000000000000) | number: '1.2-2'}} J/Th
@@ -136,7 +136,26 @@
- +
+
+ + + + +
+ + +
diff --git a/main/http_server/axe-os/src/app/components/home/home.component.ts b/main/http_server/axe-os/src/app/components/home/home.component.ts index fd65e3082..1cc84dcf9 100644 --- a/main/http_server/axe-os/src/app/components/home/home.component.ts +++ b/main/http_server/axe-os/src/app/components/home/home.component.ts @@ -1,6 +1,10 @@ -import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; -import { interval, map, Observable, shareReplay, startWith, switchMap, tap, first, Subject, takeUntil } from 'rxjs'; +import { Component, OnInit, ViewChild, Input, OnDestroy } from '@angular/core'; +import { interval, map, Observable, shareReplay, startWith, Subscription, switchMap, tap, first, Subject, takeUntil } from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ToastrService } from 'ngx-toastr'; import { HashSuffixPipe } from 'src/app/pipes/hash-suffix.pipe'; +import { ByteSuffixPipe } from 'src/app/pipes/byte-suffix.pipe'; import { QuicklinkService } from 'src/app/services/quicklink.service'; import { ShareRejectionExplanationService } from 'src/app/services/share-rejection-explanation.service'; import { LoadingService } from 'src/app/services/loading.service'; @@ -10,6 +14,10 @@ import { ISystemInfo } from 'src/models/ISystemInfo'; import { ISystemStatistics } from 'src/models/ISystemStatistics'; import { Title } from '@angular/platform-browser'; import { UIChart } from 'primeng/chart'; +import { eChartLabel } from 'src/models/enum/eChartLabel'; +import { chartLabelValue } from 'src/models/enum/eChartLabel'; +import { chartLabelKey } from 'src/models/enum/eChartLabel'; +import { LocalStorageService } from 'src/app/local-storage.service'; @Component({ selector: 'app-home', @@ -24,8 +32,9 @@ export class HomeComponent implements OnInit, OnDestroy { public chartOptions: any; public dataLabel: number[] = []; public hashrateData: number[] = []; - public temperatureData: number[] = []; public powerData: number[] = []; + public chartY1Data: number[] = []; + public chartY2Data: number[] = []; public chartData?: any; public maxPower: number = 0; @@ -46,18 +55,24 @@ export class HomeComponent implements OnInit, OnDestroy { private pageDefaultTitle: string = ''; private destroy$ = new Subject(); + private titleSubscription?: Subscription; + public form!: FormGroup; + + @Input() uri = ''; constructor( + private fb: FormBuilder, private systemService: SystemService, private themeService: ThemeService, private quickLinkService: QuicklinkService, private titleService: Title, private loadingService: LoadingService, - private shareRejectReasonsService: ShareRejectionExplanationService + private toastr: ToastrService, + private shareRejectReasonsService: ShareRejectionExplanationService, + private storageService: LocalStorageService ) { this.initializeChart(); - // Subscribe to theme changes this.themeService.getThemeSettings() .pipe(takeUntil(this.destroy$)) .subscribe(() => { @@ -65,9 +80,23 @@ export class HomeComponent implements OnInit, OnDestroy { }); } - ngOnInit() { + ngOnInit(): void { this.pageDefaultTitle = this.titleService.getTitle(); this.loadingService.loading$.next(true); + + let dataSources = this.storageService.getItem('chartDataSources'); + if (dataSources === null) { + dataSources = `{"chartY1Data":"${chartLabelKey(eChartLabel.hashrate)}",`; + dataSources += `"chartY2Data":"${chartLabelKey(eChartLabel.asicTemp)}"}`; + } + + this.form = this.fb.group(JSON.parse(dataSources)); + + this.form.valueChanges.subscribe(() => { + this.updateSystem(); + }) + + this.loadPreviousData(); } ngOnDestroy() { @@ -92,7 +121,6 @@ export class HomeComponent implements OnInit, OnDestroy { // Update chart options if (this.chartOptions) { - this.chartOptions.plugins.legend.labels.color = textColor; this.chartOptions.scales.x.ticks.color = textColorSecondary; this.chartOptions.scales.x.grid.color = surfaceBorder; this.chartOptions.scales.y.ticks.color = primaryColor; @@ -105,20 +133,39 @@ export class HomeComponent implements OnInit, OnDestroy { this.chartData = { ...this.chartData }; } + public updateSystem() { + const form = this.form.getRawValue(); + + this.storageService.setItem('chartDataSources', JSON.stringify(form)); + + this.systemService.updateSystem(this.uri, form) + .pipe(this.loadingService.lockUIUntilComplete()) + .subscribe({ + next: () => { + this.titleSubscription?.unsubscribe(); + this.clearDataPoints(); + this.loadPreviousData(); + }, + error: (err: HttpErrorResponse) => { + this.toastr.error('Error.', `Could not save chart source. ${err.message}`); + } + }); + } + private initializeChart() { const documentStyle = getComputedStyle(document.documentElement); - const textColor = documentStyle.getPropertyValue('--text-color'); const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary'); const surfaceBorder = documentStyle.getPropertyValue('--surface-border'); const primaryColor = documentStyle.getPropertyValue('--primary-color'); this.chartData = { - labels: [], + labels: [this.dataLabel], datasets: [ { type: 'line', - label: 'Hashrate', - data: [this.hashrateData], + label: eChartLabel.hashrate, + data: [this.chartY1Data], + fill: true, backgroundColor: primaryColor + '30', borderColor: primaryColor, tension: 0, @@ -126,12 +173,12 @@ export class HomeComponent implements OnInit, OnDestroy { pointHoverRadius: 5, borderWidth: 1, yAxisID: 'y', - fill: true, + hidden: false }, { type: 'line', - label: 'ASIC Temp', - data: [this.temperatureData], + label: eChartLabel.asicTemp, + data: [this.chartY2Data], fill: false, backgroundColor: textColorSecondary, borderColor: textColorSecondary, @@ -140,6 +187,7 @@ export class HomeComponent implements OnInit, OnDestroy { pointHoverRadius: 5, borderWidth: 1, yAxisID: 'y2', + hidden: false } ] }; @@ -149,23 +197,17 @@ export class HomeComponent implements OnInit, OnDestroy { maintainAspectRatio: false, plugins: { legend: { - labels: { - color: textColor - } + display: false }, tooltip: { callbacks: { label: function (tooltipItem: any) { let label = tooltipItem.dataset.label || ''; if (label) { - label += ': '; - } - if (tooltipItem.dataset.label === 'ASIC Temp') { - label += tooltipItem.raw + ' °C'; + return label += ': ' + HomeComponent.cbFormatValue(tooltipItem.raw, label); } else { - label += HashSuffixPipe.transform(tooltipItem.raw); + return tooltipItem.raw; } - return label; } } }, @@ -186,14 +228,18 @@ export class HomeComponent implements OnInit, OnDestroy { } }, y: { + type: 'linear', + display: true, + position: 'left', ticks: { color: primaryColor, - callback: (value: number) => HashSuffixPipe.transform(value) + callback: (value: number) => HomeComponent.cbFormatValue(value, this.chartData.datasets[0].label) }, grid: { color: surfaceBorder, drawBorder: false - } + }, + suggestedMax: 0 }, y2: { type: 'linear', @@ -201,7 +247,7 @@ export class HomeComponent implements OnInit, OnDestroy { position: 'right', ticks: { color: textColorSecondary, - callback: (value: number) => value + ' °C' + callback: (value: number) => HomeComponent.cbFormatValue(value, this.chartData.datasets[1].label) }, grid: { drawOnChartArea: false, @@ -213,33 +259,73 @@ export class HomeComponent implements OnInit, OnDestroy { }; this.chartData.labels = this.dataLabel; - this.chartData.datasets[0].data = this.hashrateData; - this.chartData.datasets[1].data = this.temperatureData; + this.chartData.datasets[0].data = this.chartY1Data; + this.chartData.datasets[1].data = this.chartY2Data; + } + + private loadPreviousData() + { + const chartY1DataLabel = this.form.get('chartY1Data')?.value; + const chartY2DataLabel = this.form.get('chartY2Data')?.value; // load previous data - this.stats$ = this.systemService.getStatistics() + this.stats$ = this.systemService.getStatistics(chartY1DataLabel, chartY2DataLabel) .pipe(shareReplay({ refCount: true, bufferSize: 1 })); this.stats$ .pipe(takeUntil(this.destroy$)) .subscribe(stats => { + let idxHashrate = -1; + let idxPower = -1; + let idxChartY1Data = -1; + let idxChartY2Data = -1; + let idxTimestamp = -1; + + // map label to index + for (let i = 0; i < stats.labels.length; i++) { + if (stats.labels[i] === chartLabelKey(eChartLabel.hashrate)) { idxHashrate = i; } + if (stats.labels[i] === chartLabelKey(eChartLabel.power)) { idxPower = i; } + if (stats.labels[i] === chartY1DataLabel) { idxChartY1Data = i; } + if (stats.labels[i] === chartY2DataLabel) { idxChartY2Data = i; } + if (stats.labels[i] === 'timestamp') { idxTimestamp = i; } + } + stats.statistics.forEach(element => { - const idxHashrate = 0; - const idxTemperature = 1; - const idxPower = 2; - const idxTimestamp = 3; + element[idxHashrate] = element[idxHashrate] * 1000000000; + switch (chartLabelValue(chartY1DataLabel)) { + case eChartLabel.asicVoltage: + case eChartLabel.voltage: + case eChartLabel.current: + element[idxChartY1Data] = element[idxChartY1Data] / 1000; + break; + default: + break; + } + switch (chartLabelValue(chartY2DataLabel)) { + case eChartLabel.asicVoltage: + case eChartLabel.voltage: + case eChartLabel.current: + element[idxChartY2Data] = element[idxChartY2Data] / 1000; + break; + default: + break; + } - this.hashrateData.push(element[idxHashrate] * 1000000000); - this.temperatureData.push(element[idxTemperature]); - this.powerData.push(element[idxPower]); this.dataLabel.push(new Date().getTime() - stats.currentTimestamp + element[idxTimestamp]); - - if (this.hashrateData.length >= 720) { - this.hashrateData.shift(); - this.temperatureData.shift(); - this.powerData.shift(); - this.dataLabel.shift(); + this.hashrateData.push(element[idxHashrate]); + this.powerData.push(element[idxPower]); + if (-1 != idxChartY1Data) { + this.chartY1Data.push(element[idxChartY1Data]); + } else { + this.chartY1Data.push(0.0); + } + if (-1 != idxChartY2Data) { + this.chartY2Data.push(element[idxChartY2Data]); + } else { + this.chartY2Data.push(0.0); } + + this.limitDataPoints(); }), this.startGetLiveData(); }); @@ -253,27 +339,48 @@ export class HomeComponent implements OnInit, OnDestroy { switchMap(() => { return this.systemService.getInfo() }), + map(info => { + info.hashRate = info.hashRate * 1000000000; + info.expectedHashrate = info.expectedHashrate * 1000000000; + info.voltage = info.voltage / 1000; + info.current = info.current / 1000; + info.coreVoltageActual = info.coreVoltageActual / 1000; + info.coreVoltage = info.coreVoltage / 1000; + return info; + }), tap(info => { + const chartY1DataLabel = chartLabelValue(this.form.get('chartY1Data')?.value); + const chartY2DataLabel = chartLabelValue(this.form.get('chartY2Data')?.value); + + this.maxPower = Math.max(info.maxPower, info.power); + this.nominalVoltage = info.nominalVoltage; + this.maxTemp = Math.max(75, info.temp); + this.maxFrequency = Math.max(800, info.frequency); + // Only collect and update chart data if there's no power fault if (!info.power_fault) { - this.hashrateData.push(info.hashRate * 1000000000); - this.temperatureData.push(info.temp); - this.powerData.push(info.power); this.dataLabel.push(new Date().getTime()); + this.hashrateData.push(info.hashRate); + this.powerData.push(info.power); + this.chartY1Data.push(HomeComponent.getDataForLabel(chartY1DataLabel, info)); + this.chartY2Data.push(HomeComponent.getDataForLabel(chartY2DataLabel, info)); - if ((this.hashrateData.length) >= 720) { - this.hashrateData.shift(); - this.temperatureData.shift(); - this.powerData.shift(); - this.dataLabel.shift(); - } + this.limitDataPoints(); + + this.chartData.datasets[0].label = chartY1DataLabel; + this.chartData.datasets[1].label = chartY2DataLabel; + + this.chartData.datasets[0].hidden = (chartY1DataLabel === eChartLabel.none); + this.chartData.datasets[1].hidden = (chartY2DataLabel === eChartLabel.none); + + this.chartOptions.scales.y.suggestedMax = this.getSuggestedMaxForLabel(chartY1DataLabel, info); + this.chartOptions.scales.y2.suggestedMax = this.getSuggestedMaxForLabel(chartY2DataLabel, info); + + this.chartOptions.scales.y.display = (chartY1DataLabel != eChartLabel.none); + this.chartOptions.scales.y2.display = (chartY2DataLabel != eChartLabel.none); } this.chart?.refresh(); - this.maxPower = Math.max(info.maxPower, info.power); - this.nominalVoltage = info.nominalVoltage; - this.maxTemp = Math.max(75, info.temp); - this.maxFrequency = Math.max(800, info.frequency); const isFallback = info.isUsingFallbackStratum; @@ -285,10 +392,10 @@ export class HomeComponent implements OnInit, OnDestroy { }), map(info => { info.power = parseFloat(info.power.toFixed(1)) - info.voltage = parseFloat((info.voltage / 1000).toFixed(1)); - info.current = parseFloat((info.current / 1000).toFixed(1)); - info.coreVoltageActual = parseFloat((info.coreVoltageActual / 1000).toFixed(2)); - info.coreVoltage = parseFloat((info.coreVoltage / 1000).toFixed(2)); + info.voltage = parseFloat(info.voltage.toFixed(1)); + info.current = parseFloat(info.current.toFixed(1)); + info.coreVoltageActual = parseFloat(info.coreVoltageActual.toFixed(2)); + info.coreVoltage = parseFloat(info.coreVoltage.toFixed(2)); info.temp = parseFloat(info.temp.toFixed(1)); info.temp2 = parseFloat(info.temp2.toFixed(1)); @@ -311,14 +418,14 @@ export class HomeComponent implements OnInit, OnDestroy { }) ); - this.info$ + this.titleSubscription = this.info$ .pipe(takeUntil(this.destroy$)) .subscribe(info => { this.titleService.setTitle( [ this.pageDefaultTitle, info.hostname, - (info.hashRate ? HashSuffixPipe.transform(info.hashRate * 1000000000) : false), + (info.hashRate ? HashSuffixPipe.transform(info.hashRate) : false), (info.temp ? `${info.temp}${info.temp2 > -1 ? `/${info.temp2}` : ''}${info.vrTemp ? `/${info.vrTemp}` : ''} °C` : false), (!info.power_fault ? `${info.power} W` : false), (info.bestDiff ? info.bestDiff : false), @@ -360,4 +467,89 @@ export class HomeComponent implements OnInit, OnDestroy { return this.calculateAverage(efficiencies); } + + public clearDataPoints() { + this.dataLabel.length = 0; + this.hashrateData.length = 0; + this.powerData.length = 0; + this.chartY1Data.length = 0; + this.chartY2Data.length = 0; + } + + public limitDataPoints() { + if (this.dataLabel.length >= 720) { + this.dataLabel.shift(); + this.hashrateData.shift(); + this.powerData.shift(); + this.chartY1Data.shift(); + this.chartY2Data.shift(); + } + } + + public getSuggestedMaxForLabel(label: eChartLabel | undefined, info: ISystemInfo): number { + switch (label) { + case eChartLabel.hashrate: return info.expectedHashrate; + case eChartLabel.asicTemp: return this.maxTemp; + case eChartLabel.vrTemp: return this.maxTemp + 25; + case eChartLabel.asicVoltage: return info.coreVoltage; + case eChartLabel.voltage: return (info.nominalVoltage + .5); + case eChartLabel.power: return this.maxPower; + case eChartLabel.current: return (this.maxPower / info.coreVoltage); + case eChartLabel.fanSpeed: return 100; + case eChartLabel.fanRpm: return 7000; + case eChartLabel.wifiRssi: return 0; + case eChartLabel.freeHeap: return 0; + default: return 0; + } + } + + static getDataForLabel(label: eChartLabel | undefined, info: ISystemInfo): number { + switch (label) { + case eChartLabel.hashrate: return info.hashRate; + case eChartLabel.asicTemp: return info.temp; + case eChartLabel.vrTemp: return info.vrTemp; + case eChartLabel.asicVoltage: return info.coreVoltageActual; + case eChartLabel.voltage: return info.voltage; + case eChartLabel.power: return info.power; + case eChartLabel.current: return info.current; + case eChartLabel.fanSpeed: return info.fanspeed; + case eChartLabel.fanRpm: return info.fanrpm; + case eChartLabel.wifiRssi: return info.wifiRSSI; + case eChartLabel.freeHeap: return info.freeHeap; + default: return 0.0; + } + } + + static getSettingsForLabel(label: eChartLabel): {suffix: string; precision: number} { + switch (label) { + case eChartLabel.hashrate: return {suffix: ' H/s', precision: 0}; + case eChartLabel.asicTemp: return {suffix: ' °C', precision: 1}; + case eChartLabel.vrTemp: return {suffix: ' °C', precision: 1}; + case eChartLabel.asicVoltage: return {suffix: ' V', precision: 3}; + case eChartLabel.voltage: return {suffix: ' V', precision: 1}; + case eChartLabel.power: return {suffix: ' W', precision: 1}; + case eChartLabel.current: return {suffix: ' A', precision: 1}; + case eChartLabel.fanSpeed: return {suffix: ' %', precision: 0}; + case eChartLabel.fanRpm: return {suffix: ' rpm', precision: 0}; + case eChartLabel.wifiRssi: return {suffix: ' dBm', precision: 0}; + case eChartLabel.freeHeap: return {suffix: ' B', precision: 0}; + default: return {suffix: '', precision: 0}; + } + } + + static cbFormatValue(value: number, datasetLabel: eChartLabel): string { + switch (datasetLabel) { + case eChartLabel.hashrate: return HashSuffixPipe.transform(value); + case eChartLabel.freeHeap: return ByteSuffixPipe.transform(value); + default: + const settings = HomeComponent.getSettingsForLabel(datasetLabel); + return value.toFixed(settings.precision) + settings.suffix; + } + } + + dataSourceLabels(info: ISystemInfo) { + return Object.entries(eChartLabel) + .filter(([key, ]) => key !== 'vrTemp' || info.vrTemp) + .map(([key, value]) => ({name: value, value: key})); + } } diff --git a/main/http_server/axe-os/src/app/pipes/byte-suffix.pipe.spec.ts b/main/http_server/axe-os/src/app/pipes/byte-suffix.pipe.spec.ts new file mode 100644 index 000000000..1c05b4c86 --- /dev/null +++ b/main/http_server/axe-os/src/app/pipes/byte-suffix.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ByteSuffixPipe } from './byte-suffix.pipe'; + +describe('ByteSuffixPipe', () => { + it('create an instance', () => { + const pipe = new ByteSuffixPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/main/http_server/axe-os/src/app/pipes/byte-suffix.pipe.ts b/main/http_server/axe-os/src/app/pipes/byte-suffix.pipe.ts new file mode 100644 index 000000000..ae1c94363 --- /dev/null +++ b/main/http_server/axe-os/src/app/pipes/byte-suffix.pipe.ts @@ -0,0 +1,39 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'byteSuffix' +}) +export class ByteSuffixPipe implements PipeTransform { + + private static _this = new ByteSuffixPipe(); + + public static transform(value: number): string { + return this._this.transform(value); + } + + public transform(value: number): string { + + if (value == null || value < 0) { + return '0'; + } + + const suffixes = [' B', ' kB', ' MB', ' GB', ' TB', ' PB', ' EB']; + + let power = Math.floor(Math.log10(value) / 3); + if (power < 0) { + power = 0; + } + const scaledValue = value / Math.pow(1000, power); + const suffix = suffixes[power]; + + if (scaledValue < 10) { + return scaledValue.toFixed(2) + suffix; + } else if (scaledValue < 100) { + return scaledValue.toFixed(1) + suffix; + } + + return scaledValue.toFixed(0) + suffix; + } + + +} diff --git a/main/http_server/axe-os/src/app/services/system.service.ts b/main/http_server/axe-os/src/app/services/system.service.ts index 2df268f38..d1f557eff 100644 --- a/main/http_server/axe-os/src/app/services/system.service.ts +++ b/main/http_server/axe-os/src/app/services/system.service.ts @@ -1,6 +1,9 @@ -import { HttpClient, HttpEvent } from '@angular/common/http'; +import { HttpClient, HttpParams, HttpEvent } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { delay, Observable, of } from 'rxjs'; +import { eChartLabel } from 'src/models/enum/eChartLabel'; +import { chartLabelKey } from 'src/models/enum/eChartLabel'; +import { chartLabelValue } from 'src/models/enum/eChartLabel'; import { ISystemInfo } from 'src/models/ISystemInfo'; import { ISystemStatistics } from 'src/models/ISystemStatistics'; import { ISystemASIC } from 'src/models/ISystemASIC'; @@ -76,10 +79,10 @@ export class SystemService { displayTimeout: -1, autofanspeed: 1, minFanSpeed: 25, - fanspeed: 100, + fanspeed: 50, temptarget: 60, statsFrequency: 30, - fanrpm: 0, + fanrpm: 3583, boardtemp1: 30, boardtemp2: 40, @@ -88,27 +91,69 @@ export class SystemService { ).pipe(delay(1000)); } - public getStatistics(uri: string = ''): Observable { + public getStatistics(y1: string, y2: string, uri: string = ''): Observable { + let columnList = [chartLabelKey(eChartLabel.hashrate), chartLabelKey(eChartLabel.power)]; + + if ((y1 != chartLabelKey(eChartLabel.hashrate)) && (y1 != chartLabelKey(eChartLabel.power))) { + columnList.push(y1); + } + if ((y2 != chartLabelKey(eChartLabel.hashrate)) && (y2 != chartLabelKey(eChartLabel.power))) { + columnList.push(y2); + } + if (environment.production) { - return this.httpClient.get(`${uri}/api/system/statistics/dashboard`) as Observable; + const options = { params: new HttpParams().set('columns', columnList.join(',')) }; + return this.httpClient.get(`${uri}/api/system/statistics`, options) as Observable; } // Mock data for development + const hashrateData = [0,413.4903744405481,410.7764830376959,440.100549473198,430.5816012914026,452.5464981767163,414.9564271189586,498.7294609150379,411.1671601439723,491.327834852684]; + const powerData = [14.45068359375,14.86083984375,15.03173828125,15.1171875,15.1171875,15.1513671875,15.185546875,15.27099609375,15.30517578125,15.33935546875]; + const asicTempData = [-1,58.5,59.625,60.125,60.75,61.5,61.875,62.125,62.5,63]; + const vrTempData = [45,45,45,44,45,44,44,45,45,45]; + const asicVoltageData = [1221,1223,1219,1223,1217,1222,1221,1219,1221,1221]; + const voltageData = [5196.875,5204.6875,5196.875,5196.875,5196.875,5196.875,5196.875,5196.875,5196.875,5204.6875]; + const currentData = [2284.375,2284.375,2253.125,2284.375,2253.125,2231.25,2284.375,2253.125,2253.125,2284.375]; + const fanSpeedData = [48,52,50,52,53,54,50,50,48,48]; + const fanRpmData = [4032,3545,3904,3691,3564,3554,3691,3573,3701,4044]; + const wifiRssiData = [-35,-34,-33,-34,-34,-34,-33,-35,-33,-34]; + const freeHeapData = [214504,212504,213504,210504,207504,209504,203504,202504,201504,200504]; + const timestampData = [13131,18126,23125,28125,33125,38125,43125,48125,53125,58125]; + + columnList.push("timestamp"); + let statisticsList: number[][] = []; + + for(let i: number = 0; i < 10; i++) { + statisticsList[i] = []; + for(let j: number = 0; j < columnList.length; j++) { + switch (chartLabelValue(columnList[j])) { + case eChartLabel.hashrate: statisticsList[i][j] = hashrateData[i]; break; + case eChartLabel.power: statisticsList[i][j] = powerData[i]; break; + case eChartLabel.asicTemp: statisticsList[i][j] = asicTempData[i]; break; + case eChartLabel.vrTemp: statisticsList[i][j] = vrTempData[i]; break; + case eChartLabel.asicVoltage: statisticsList[i][j] = asicVoltageData[i]; break; + case eChartLabel.voltage: statisticsList[i][j] = voltageData[i]; break; + case eChartLabel.current: statisticsList[i][j] = currentData[i]; break; + case eChartLabel.fanSpeed: statisticsList[i][j] = fanSpeedData[i]; break; + case eChartLabel.fanRpm: statisticsList[i][j] = fanRpmData[i]; break; + case eChartLabel.wifiRssi: statisticsList[i][j] = wifiRssiData[i]; break; + case eChartLabel.freeHeap: statisticsList[i][j] = freeHeapData[i]; break; + default: + if (columnList[j] === "timestamp") { + statisticsList[i][j] = timestampData[i]; + } else { + statisticsList[i][j] = 0; + } + break; + } + } + } + return of({ currentTimestamp: 61125, - statistics: [ - [0,-1,14.45068359375,13131], - [413.4903744405481,58.5,14.86083984375,18126], - [410.7764830376959,59.625,15.03173828125,23125], - [440.100549473198,60.125,15.1171875,28125], - [430.5816012914026,60.75,15.1171875,33125], - [452.5464981767163,61.5,15.1513671875,38125], - [414.9564271189586,61.875,15.185546875,43125], - [498.7294609150379,62.125,15.27099609375,48125], - [411.1671601439723,62.5,15.30517578125,53125], - [491.327834852684,63,15.33935546875,58125] - ] - }).pipe(delay(1000)); + labels: columnList, + statistics: statisticsList + }); } public restart(uri: string = '') { diff --git a/main/http_server/axe-os/src/models/ISystemStatistics.ts b/main/http_server/axe-os/src/models/ISystemStatistics.ts index dd1e0da44..f8ad6f8e5 100644 --- a/main/http_server/axe-os/src/models/ISystemStatistics.ts +++ b/main/http_server/axe-os/src/models/ISystemStatistics.ts @@ -1,4 +1,5 @@ export interface ISystemStatistics { currentTimestamp: number; + labels: string[]; statistics: number[][]; } diff --git a/main/http_server/axe-os/src/models/enum/eChartLabel.ts b/main/http_server/axe-os/src/models/enum/eChartLabel.ts new file mode 100644 index 000000000..c7b59ba12 --- /dev/null +++ b/main/http_server/axe-os/src/models/enum/eChartLabel.ts @@ -0,0 +1,22 @@ +export enum eChartLabel { + hashrate = 'Hashrate', + asicTemp = 'ASIC Temp', + vrTemp = 'VR Temp', + asicVoltage = 'ASIC Voltage', + voltage = 'Voltage', + power = 'Power', + current = 'Current', + fanSpeed = 'Fan Speed', + fanRpm = 'Fan RPM', + wifiRssi = 'Wi-Fi RSSI', + freeHeap = 'Free Heap', + none = 'None' +} + +export function chartLabelValue(enumKey: string) { + return Object.entries(eChartLabel).find(([key, val]) => key === enumKey)?.[1]; +} + +export function chartLabelKey(value: eChartLabel): string { + return Object.keys(eChartLabel)[Object.values(eChartLabel).indexOf(value)]; +} diff --git a/main/http_server/axe-os/src/styles.scss b/main/http_server/axe-os/src/styles.scss index 7ae5da1e3..db7fbd313 100644 --- a/main/http_server/axe-os/src/styles.scss +++ b/main/http_server/axe-os/src/styles.scss @@ -155,6 +155,8 @@ button.color-dot { background: var(--surface-overlay) !important; .p-dropdown-items { + padding: 0; + .p-dropdown-item { &:hover { background: var(--highlight-bg) !important; @@ -168,6 +170,21 @@ button.color-dot { } } } + + .p-overlay { + left: -1px !important; + right: -1px !important; + } + + &--small { + .p-dropdown-label, + .p-dropdown-item { + font-size: small; + } + } + &--border-color-primary { + border-color: var(--primary-color); + } } .p-button-icon { diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c index b6b4278d3..484194b88 100644 --- a/main/http_server/http_server.c +++ b/main/http_server/http_server.c @@ -44,13 +44,60 @@ #include "websocket.h" #define JSON_ALL_STATS_ELEMENT_SIZE 120 -#define JSON_DASHBOARD_STATS_ELEMENT_SIZE 60 static const char * TAG = "http_server"; static const char * CORS_TAG = "CORS"; static char axeOSVersion[32]; +static const char * STATS_LABEL_HASHRATE = "hashrate"; +static const char * STATS_LABEL_ASIC_TEMP = "asicTemp"; +static const char * STATS_LABEL_VR_TEMP = "vrTemp"; +static const char * STATS_LABEL_ASIC_VOLTAGE = "asicVoltage"; +static const char * STATS_LABEL_VOLTAGE = "voltage"; +static const char * STATS_LABEL_POWER = "power"; +static const char * STATS_LABEL_CURRENT = "current"; +static const char * STATS_LABEL_FAN_SPEED = "fanSpeed"; +static const char * STATS_LABEL_FAN_RPM = "fanRpm"; +static const char * STATS_LABEL_WIFI_RSSI = "wifiRssi"; +static const char * STATS_LABEL_FREE_HEAP = "freeHeap"; + +static const char * STATS_LABEL_TIMESTAMP = "timestamp"; + +typedef enum +{ + SRC_HASHRATE, + SRC_ASIC_TEMP, + SRC_VR_TEMP, + SRC_ASIC_VOLTAGE, + SRC_VOLTAGE, + SRC_POWER, + SRC_CURRENT, + SRC_FAN_SPEED, + SRC_FAN_RPM, + SRC_WIFI_RSSI, + SRC_FREE_HEAP, + SRC_NONE // last +} DataSource; + +DataSource strToDataSource(const char * sourceStr) +{ + if (NULL != sourceStr) { + if (strcmp(sourceStr, STATS_LABEL_HASHRATE) == 0) return SRC_HASHRATE; + if (strcmp(sourceStr, STATS_LABEL_VOLTAGE) == 0) return SRC_VOLTAGE; + if (strcmp(sourceStr, STATS_LABEL_POWER) == 0) return SRC_POWER; + if (strcmp(sourceStr, STATS_LABEL_CURRENT) == 0) return SRC_CURRENT; + if (strcmp(sourceStr, STATS_LABEL_ASIC_TEMP) == 0) return SRC_ASIC_TEMP; + if (strcmp(sourceStr, STATS_LABEL_VR_TEMP) == 0) return SRC_VR_TEMP; + if (strcmp(sourceStr, STATS_LABEL_ASIC_VOLTAGE) == 0) return SRC_ASIC_VOLTAGE; + if (strcmp(sourceStr, STATS_LABEL_FAN_SPEED) == 0) return SRC_FAN_SPEED; + if (strcmp(sourceStr, STATS_LABEL_FAN_RPM) == 0) return SRC_FAN_RPM; + if (strcmp(sourceStr, STATS_LABEL_WIFI_RSSI) == 0) return SRC_WIFI_RSSI; + if (strcmp(sourceStr, STATS_LABEL_FREE_HEAP) == 0) return SRC_FREE_HEAP; + } + return SRC_NONE; +} + static GlobalState * GLOBAL_STATE; static httpd_handle_t server = NULL; @@ -739,84 +786,6 @@ static esp_err_t GET_system_info(httpd_req_t * req) return ESP_OK; } -int create_json_statistics_all(cJSON * root) -{ - int prebuffer = 0; - - if (root) { - // create array for all statistics - const char *label[12] = { - "hashRate", "temp", "vrTemp", "power", "voltage", - "current", "coreVoltageActual", "fanspeed", "fanrpm", - "wifiRSSI", "freeHeap", "timestamp" - }; - - cJSON * statsLabelArray = cJSON_CreateStringArray(label, 12); - cJSON_AddItemToObject(root, "labels", statsLabelArray); - prebuffer++; - - cJSON * statsArray = cJSON_AddArrayToObject(root, "statistics"); - - if (NULL != GLOBAL_STATE->STATISTICS_MODULE.statisticsList) { - StatisticsNodePtr node = *GLOBAL_STATE->STATISTICS_MODULE.statisticsList; // double pointer - struct StatisticsData statsData; - - while (NULL != node) { - node = statisticData(node, &statsData); - - cJSON *valueArray = cJSON_CreateArray(); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.hashrate)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.chipTemperature)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.vrTemperature)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.power)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.voltage)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.current)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.coreVoltageActual)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.fanSpeed)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.fanRPM)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.wifiRSSI)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.freeHeap)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.timestamp)); - - cJSON_AddItemToArray(statsArray, valueArray); - prebuffer++; - } - } - } - - return prebuffer; -} - -int create_json_statistics_dashboard(cJSON * root) -{ - int prebuffer = 0; - - if (root) { - // create array for dashboard statistics - cJSON * statsArray = cJSON_AddArrayToObject(root, "statistics"); - - if (NULL != GLOBAL_STATE->STATISTICS_MODULE.statisticsList) { - StatisticsNodePtr node = *GLOBAL_STATE->STATISTICS_MODULE.statisticsList; // double pointer - struct StatisticsData statsData; - - while (NULL != node) { - node = statisticData(node, &statsData); - - cJSON *valueArray = cJSON_CreateArray(); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.hashrate)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.chipTemperature)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.power)); - cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.timestamp)); - - cJSON_AddItemToArray(statsArray, valueArray); - prebuffer++; - } - } - } - - return prebuffer; -} - static esp_err_t GET_system_statistics(httpd_req_t * req) { if (is_network_allowed(req) != ESP_OK) { @@ -831,42 +800,95 @@ static esp_err_t GET_system_statistics(httpd_req_t * req) return ESP_OK; } - cJSON * root = cJSON_CreateObject(); - cJSON_AddNumberToObject(root, "currentTimestamp", (esp_timer_get_time() / 1000)); - int prebuffer = 1; - - prebuffer += create_json_statistics_all(root); - - const char * response = cJSON_PrintBuffered(root, (JSON_ALL_STATS_ELEMENT_SIZE * prebuffer), 0); // unformatted - httpd_resp_sendstr(req, response); - free((void *)response); - - cJSON_Delete(root); - - return ESP_OK; -} + char * buf = NULL; + size_t bufLen = httpd_req_get_url_query_len(req) + 1; + bool dataSelection[SRC_NONE] = {false}; + bool selectionCheck = false; + int prebuffer = 0; -static esp_err_t GET_system_statistics_dashboard(httpd_req_t * req) -{ - if (is_network_allowed(req) != ESP_OK) { - return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized"); + // Check query parameters + if (1 < bufLen) { + buf = (char *)malloc(bufLen); + if (buf) { + if (httpd_req_get_url_query_str(req, buf, bufLen) == ESP_OK) { + char * columns = (char *)malloc(bufLen); + if (columns) { + if (httpd_query_key_value(buf, "columns", columns, bufLen) == ESP_OK) { + char * param = strtok(columns, ","); + while (NULL != param) { + DataSource sourceParam = strToDataSource(param); + if (SRC_NONE != sourceParam) { + dataSelection[sourceParam] = true; + selectionCheck = true; + } + param = strtok(NULL, ","); + } + } + free((void *)columns); + } + } + free((void *)buf); + } } - httpd_resp_set_type(req, "application/json"); - - // Set CORS headers - if (set_cors_headers(req) != ESP_OK) { - httpd_resp_send_500(req); - return ESP_OK; + if (!selectionCheck) { + // Enable all + for (int i = 0; i < SRC_NONE; i++) { + dataSelection[i] = true; + } } + // Create object for statistics cJSON * root = cJSON_CreateObject(); cJSON_AddNumberToObject(root, "currentTimestamp", (esp_timer_get_time() / 1000)); - int prebuffer = 1; - - prebuffer += create_json_statistics_dashboard(root); + prebuffer++; + + cJSON * labelArray = cJSON_CreateArray(); + if (dataSelection[SRC_HASHRATE]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_HASHRATE)); } + if (dataSelection[SRC_ASIC_TEMP]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_ASIC_TEMP)); } + if (dataSelection[SRC_VR_TEMP]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_VR_TEMP)); } + if (dataSelection[SRC_ASIC_VOLTAGE]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_ASIC_VOLTAGE)); } + if (dataSelection[SRC_VOLTAGE]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_VOLTAGE)); } + if (dataSelection[SRC_POWER]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_POWER)); } + if (dataSelection[SRC_CURRENT]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_CURRENT)); } + if (dataSelection[SRC_FAN_SPEED]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_FAN_SPEED)); } + if (dataSelection[SRC_FAN_RPM]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_FAN_RPM)); } + if (dataSelection[SRC_WIFI_RSSI]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_WIFI_RSSI)); } + if (dataSelection[SRC_FREE_HEAP]) { cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_FREE_HEAP)); } + cJSON_AddItemToArray(labelArray, cJSON_CreateString(STATS_LABEL_TIMESTAMP)); + + cJSON_AddItemToObject(root, "labels", labelArray); + prebuffer++; + + cJSON * statsArray = cJSON_AddArrayToObject(root, "statistics"); + + if (NULL != GLOBAL_STATE->STATISTICS_MODULE.statisticsList) { + StatisticsNodePtr node = *GLOBAL_STATE->STATISTICS_MODULE.statisticsList; // double pointer + struct StatisticsData statsData; + + while (NULL != node) { + node = statisticData(node, &statsData); + + cJSON * valueArray = cJSON_CreateArray(); + if (dataSelection[SRC_HASHRATE]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.hashrate)); } + if (dataSelection[SRC_ASIC_TEMP]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.chipTemperature)); } + if (dataSelection[SRC_VR_TEMP]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.vrTemperature)); } + if (dataSelection[SRC_ASIC_VOLTAGE]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.coreVoltageActual)); } + if (dataSelection[SRC_VOLTAGE]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.voltage)); } + if (dataSelection[SRC_POWER]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.power)); } + if (dataSelection[SRC_CURRENT]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.current)); } + if (dataSelection[SRC_FAN_SPEED]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.fanSpeed)); } + if (dataSelection[SRC_FAN_RPM]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.fanRPM)); } + if (dataSelection[SRC_WIFI_RSSI]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.wifiRSSI)); } + if (dataSelection[SRC_FREE_HEAP]) { cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.freeHeap)); } + cJSON_AddItemToArray(valueArray, cJSON_CreateNumber(statsData.timestamp)); + + cJSON_AddItemToArray(statsArray, valueArray); + prebuffer++; + } + } - const char * response = cJSON_PrintBuffered(root, (JSON_DASHBOARD_STATS_ELEMENT_SIZE * prebuffer), 0); // unformatted + const char * response = cJSON_PrintBuffered(root, (JSON_ALL_STATS_ELEMENT_SIZE * prebuffer), 0); // unformatted httpd_resp_sendstr(req, response); free((void *)response); @@ -1104,15 +1126,6 @@ esp_err_t start_rest_server(void * pvParameters) }; httpd_register_uri_handler(server, &system_statistics_get_uri); - /* URI handler for fetching system statistic values for dashboard */ - httpd_uri_t system_statistics_dashboard_get_uri = { - .uri = "/api/system/statistics/dashboard", - .method = HTTP_GET, - .handler = GET_system_statistics_dashboard, - .user_ctx = rest_context - }; - httpd_register_uri_handler(server, &system_statistics_dashboard_get_uri); - /* URI handler for WiFi scan */ httpd_uri_t wifi_scan_get_uri = { .uri = "/api/system/wifi/scan", diff --git a/main/http_server/openapi.yaml b/main/http_server/openapi.yaml index 93bbcc9c1..d0acdb157 100644 --- a/main/http_server/openapi.yaml +++ b/main/http_server/openapi.yaml @@ -595,6 +595,16 @@ paths: summary: Get system statistics description: Returns system statistics operationId: getSystemStatistics + parameters: + - in: query + name: columns + required: false + schema: + type: array + items: + type: string + example: hashrate,asicTemp,vrTemp,asicVoltage,voltage,power,current,fanSpeed,fanRpm,wifiRssi,freeHeap + description: List of labels for which data should be retrieved tags: - system responses: @@ -630,40 +640,6 @@ paths: '500': description: Internal server error - /api/system/statistics/dashboard: - get: - summary: Get system statistics for dashboard - description: Returns system statistics for dashboard - operationId: getSystemStatisticsDashboard - tags: - - system - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: object - required: - - currentTimestamp - - statistics - properties: - currentTimestamp: - type: number - description: Current timestamp as a reference - statistics: - type: array - description: Statistics data point(s) - items: - type: array - description: Statistics data values(s) - items: - type: number - '401': - description: Unauthorized - Client not in allowed network range - '500': - description: Internal server error - /api/system/restart: post: summary: Restart the system