diff --git a/main/global_state.h b/main/global_state.h
index 360c3995d8..3561504038 100644
--- a/main/global_state.h
+++ b/main/global_state.h
@@ -93,6 +93,10 @@ typedef struct SystemModule
bool use_fallback_stratum;
bool is_using_fallback;
float response_time;
+ float response_time_min;
+ float response_time_max;
+ float last_response_time_min;
+ float last_response_time_max;
uint16_t response_share_batch;
float process_time;
float cpu_usage;
diff --git a/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.html b/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.html
new file mode 100644
index 0000000000..8db5ab0b04
--- /dev/null
+++ b/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.html
@@ -0,0 +1,38 @@
+
+
+ {{ selectedOptionName }}
+
+
+ @if (isOpen) {
+
+ @for (option of options; track option.value ?? $index; let i = $index) {
+ -
+ {{ option.name || option.label }}
+
+ }
+
+ }
+
diff --git a/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.ts b/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.ts
index 0a62a8b6d1..f31871325f 100644
--- a/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.ts
+++ b/main/http_server/axe-os/src/app/components/dropdown/dropdown.component.ts
@@ -5,46 +5,7 @@ import { SelectOption } from '../../models/select-option.model';
@Component({
selector: 'app-dropdown',
- template: `
-
-
- {{ selectedOptionName }}
-
-
- @if (isOpen) {
-
- @for (option of options; track option.value ?? $index; let i = $index) {
- -
- {{ option.name || option.label }}
-
- }
-
- }
-
- `,
+ templateUrl: './dropdown.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
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 915a47cb27..e7eda471b9 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
@@ -221,12 +221,12 @@
+ class="w-48 text-sm">
+ class="w-48 text-sm">
diff --git a/main/http_server/axe-os/src/app/components/home/home.component.spec.ts b/main/http_server/axe-os/src/app/components/home/home.component.spec.ts
index 451f1150a0..c8a01419cb 100644
--- a/main/http_server/axe-os/src/app/components/home/home.component.spec.ts
+++ b/main/http_server/axe-os/src/app/components/home/home.component.spec.ts
@@ -36,6 +36,7 @@ import { LocalStorageService } from 'src/app/local-storage.service';
import { DashboardEditService } from 'src/app/services/dashboard-edit.service';
import { LayoutService } from 'src/app/layout/service/app.layout.service';
import { SystemInfo as ISystemInfo, SystemStatistics as ISystemStatistics } from 'src/app/generated/models';
+import { eChartLabel } from 'src/models/enum/eChartLabel';
const mockSystemInfo: ISystemInfo = {
ASICModel: 'BM1370',
@@ -110,6 +111,8 @@ const mockSystemInfo: ISystemInfo = {
maxPower: 20,
poolConnectionInfo: 'Connected',
responseTime: 45,
+ responseTimeMin: 45,
+ responseTimeMax: 45,
responseShareBatch: 1,
poolDifficulty: 1000,
blockHeight: 800000,
@@ -510,6 +513,46 @@ describe('HomeComponent', () => {
expect(component.dataLabel.length).toBe(2);
});
+ it('should merge response time into closest neighbor when removed point has data in limitDataPoints', () => {
+ component['statsLimit'] = 3;
+ component.dataLabel = [1000, 2000, 2050, 4000];
+ component.hashrateData = [100, 100, 100, 100];
+ component.powerData = [10, 10, 10, 10];
+ component.chartDatasets = {
+ responseTimeMin: [null, 35.5, null, null],
+ responseTimeMax: [null, 55.5, null, null]
+ };
+
+ component.limitDataPoints(0);
+
+ expect(component.dataLabel.length).toBe(3);
+ expect(component.dataLabel).not.toContain(2000);
+ expect(component.dataLabel).toContain(2050);
+ const idx2050 = component.dataLabel.indexOf(2050);
+ expect(component.chartDatasets['responseTimeMin'][idx2050]).toBe(35.5);
+ expect(component.chartDatasets['responseTimeMax'][idx2050]).toBe(55.5);
+ });
+
+ it('should merge min and max of either points when thinning in limitDataPoints', () => {
+ component['statsLimit'] = 3;
+ component.dataLabel = [1000, 2000, 2050, 4000];
+ component.hashrateData = [100, 100, 100, 100];
+ component.powerData = [10, 10, 10, 10];
+ component.chartDatasets = {
+ responseTimeMin: [null, 35.5, 25.0, null],
+ responseTimeMax: [null, 45.5, 60.0, null]
+ };
+
+ component.limitDataPoints(0);
+
+ expect(component.dataLabel.length).toBe(3);
+ expect(component.dataLabel).toContain(2050);
+ expect(component.dataLabel).not.toContain(2000);
+ const idx2050 = component.dataLabel.indexOf(2050);
+ expect(component.chartDatasets['responseTimeMin'][idx2050]).toBe(25.0);
+ expect(component.chartDatasets['responseTimeMax'][idx2050]).toBe(60.0);
+ });
+
it('should clear flash timeouts on destroy', () => {
component['shareAcceptedTimeout'] = setTimeout(() => {}, 10000) as any;
component['shareRejectedTimeout'] = setTimeout(() => {}, 10000) as any;
@@ -523,5 +566,13 @@ describe('HomeComponent', () => {
expect(clearTimeout).toHaveBeenCalledWith(component['shareRejectedTimeout']);
expect(clearTimeout).toHaveBeenCalledWith(component['workReceivedTimeout']);
});
+
+ it('should return 0 for response time min/max and undefined for others in getMinForLabel', () => {
+ expect(component.getMinForLabel(eChartLabel.responseTimeMin)).toBe(0);
+ expect(component.getMinForLabel(eChartLabel.responseTimeMax)).toBe(0);
+ expect(component.getMinForLabel(eChartLabel.hashrate)).toBeUndefined();
+ expect(component.getMinForLabel(eChartLabel.none)).toBeUndefined();
+ expect(component.getMinForLabel(undefined)).toBeUndefined();
+ });
});
});
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 8ee665e766..cb21f29751 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
@@ -88,7 +88,7 @@ export class HomeComponent implements OnInit, OnDestroy {
public dataLabel: number[] = [];
public hashrateData: number[] = [];
public powerData: number[] = [];
- public chartDatasets: { [key: string]: number[] } = {};
+ public chartDatasets: { [key: string]: (number | null)[] } = {};
public chartUnitGroups = ChartUnitGroups;
public chartHiddenSensors: Record = {};
public chartData?: any;
@@ -525,15 +525,25 @@ export class HomeComponent implements OnInit, OnDestroy {
}
}
- private createChartDatasets(
- formControlName: 'chartY1Unit' | 'chartY2Unit',
- baseColor: string,
- mixColor: string,
- fill: boolean,
- yAxisID: 'y' | 'y2'
- ): any[] {
+ private createChartDatasets(yAxisID: 'y' | 'y2'): any[] {
+ const isY1 = yAxisID === 'y';
+ const formControlName = isY1 ? 'chartY1Unit' : 'chartY2Unit';
const unit = this.form?.get(formControlName)?.value;
- const labels = ChartUnitGroups.find(g => g.value === unit)?.labels || [];
+ const group = ChartUnitGroups.find(g => g.value === unit);
+ const labels = group?.labels || [];
+ const isScatter = group?.type === 'scatter';
+
+ const showLine = !isScatter;
+ const fill = isY1 && !isScatter;
+ const pointRadius = isScatter ? 3 : 2;
+ const borderWidth = isScatter ? 0 : 1;
+
+ const documentStyle = getComputedStyle(document.documentElement);
+ const textColorSecondary = documentStyle.getPropertyValue('--color-text-secondary').trim() || '#808080';
+ const baseColor = isY1
+ ? (documentStyle.getPropertyValue('--color-primary').trim() || '#F80421')
+ : (documentStyle.getPropertyValue('--chart-axis2-color').trim() || textColorSecondary);
+ const mixColor = documentStyle.getPropertyValue('--color-text-main').trim() || '#ffffff';
const entries = labels
.filter(label => this.isSensorSupported(label, this.latestInfo))
@@ -553,7 +563,7 @@ export class HomeComponent implements OnInit, OnDestroy {
const fillIndex = fill ? entries.findIndex(entry => !entry.hidden) : -1;
return entries.map(({ labelKey, label, index, hidden }) => {
- const borderColor = index === 0
+ const borderColor = (index === 0 || isScatter)
? baseColor
: `color-mix(in srgb, ${baseColor} ${100 - index * 15}%, ${mixColor} ${index * 15}%)`;
const backgroundColor = `color-mix(in srgb, ${borderColor}, transparent 81%)`;
@@ -565,10 +575,13 @@ export class HomeComponent implements OnInit, OnDestroy {
fill: index === fillIndex,
backgroundColor,
borderColor,
+ pointBackgroundColor: borderColor,
+ pointBorderColor: borderColor,
tension: 0,
- pointRadius: 2,
+ showLine,
+ pointRadius,
pointHoverRadius: 5,
- borderWidth: 1,
+ borderWidth,
yAxisID,
hidden
};
@@ -582,23 +595,16 @@ export class HomeComponent implements OnInit, OnDestroy {
*/
private refreshChartFill(): void {
const onFilledAxis = (this.chartData?.datasets ?? []).filter((dataset: any) => dataset.yAxisID === 'y');
- const target = onFilledAxis.find((dataset: any) => !dataset.hidden);
+ const target = onFilledAxis.find((dataset: any) => !dataset.hidden && dataset.showLine !== false);
onFilledAxis.forEach((dataset: any) => {
- dataset.fill = dataset === target;
+ dataset.fill = (dataset === target && dataset.showLine !== false);
});
}
private rebuildChartDatasets() {
- const documentStyle = getComputedStyle(document.documentElement);
- const primaryColor = documentStyle.getPropertyValue('--color-primary').trim() || '#F80421';
- const textColor = documentStyle.getPropertyValue('--color-text-main').trim() || '#ffffff';
- const textColorSecondary = documentStyle.getPropertyValue('--color-text-secondary').trim() || '#808080';
- const axis2Color = documentStyle.getPropertyValue('--chart-axis2-color').trim() || textColorSecondary;
-
const datasets = [
- ...this.createChartDatasets('chartY1Unit', primaryColor, textColor, true, 'y'),
- // 'black' as the mix colour darkened the series into the dark background.
- ...this.createChartDatasets('chartY2Unit', axis2Color, textColor, false, 'y2')
+ ...this.createChartDatasets('y'),
+ ...this.createChartDatasets('y2')
];
if (this.chartData) {
@@ -659,6 +665,7 @@ export class HomeComponent implements OnInit, OnDestroy {
const textColorSecondary = documentStyle.getPropertyValue('--color-text-secondary').trim();
const surfaceBorder = documentStyle.getPropertyValue('--color-border-content').trim();
const primaryColor = documentStyle.getPropertyValue('--color-primary').trim();
+ const axis2Color = documentStyle.getPropertyValue('--chart-axis2-color').trim() || textColorSecondary;
this.primaryColorRgb = this.hexToRgb(primaryColor);
this.chartData = {
@@ -712,6 +719,7 @@ export class HomeComponent implements OnInit, OnDestroy {
}
},
tooltip: {
+ filter: (tooltipItem: any) => tooltipItem.raw !== null && tooltipItem.raw !== undefined,
callbacks: {
label: function (tooltipItem: any) {
let label = tooltipItem.dataset.label || '';
@@ -808,7 +816,7 @@ export class HomeComponent implements OnInit, OnDestroy {
display: true,
position: 'right',
ticks: {
- color: textColorSecondary,
+ color: axis2Color,
callback: (value: number) => {
const y2Dataset = this.chartData?.datasets?.find((d: any) => d.yAxisID === 'y2');
return y2Dataset?.label ? HomeComponent.cbFormatValue(value, y2Dataset.label, {tickmark: true}) : value.toString();
@@ -850,8 +858,8 @@ export class HomeComponent implements OnInit, OnDestroy {
stats.labels.forEach((labelKey, labelIdx) => {
const valEnum = chartLabelValue(labelKey);
if (valEnum === eChartLabel.asicVoltage || valEnum === eChartLabel.voltage || valEnum === eChartLabel.current) {
- stats.statistics.forEach((element: number[]) => {
- if (element[labelIdx] !== undefined) {
+ stats.statistics.forEach((element: (number | null)[]) => {
+ if (element[labelIdx] !== undefined && element[labelIdx] !== null) {
element[labelIdx] = element[labelIdx] / 1000;
}
});
@@ -860,15 +868,19 @@ export class HomeComponent implements OnInit, OnDestroy {
this.lastStatsFrequency = 0;
if (stats.statistics.length >= 2 && idxTimestamp !== -1) {
- const totalDurationMs = stats.statistics[stats.statistics.length - 1][idxTimestamp] - stats.statistics[0][idxTimestamp];
- this.lastStatsFrequency = Math.floor(totalDurationMs / (stats.statistics.length - 1) / 1000);
+ const lastTs = stats.statistics[stats.statistics.length - 1][idxTimestamp];
+ const firstTs = stats.statistics[0][idxTimestamp];
+ if (lastTs !== null && firstTs !== null) {
+ const totalDurationMs = lastTs - firstTs;
+ this.lastStatsFrequency = Math.floor(totalDurationMs / (stats.statistics.length - 1) / 1000);
+ }
}
// 1. Gather existing points only if we are not clearing
const existingPoints = clear ? [] : this.dataLabel.map((timestamp, i) => {
- const values: Record = {};
+ const values: Record = {};
allLabels.forEach(labelKey => {
- values[labelKey] = this.chartDatasets[labelKey]?.[i] ?? 0;
+ values[labelKey] = this.chartDatasets[labelKey]?.[i] ?? null;
});
return {
timestamp,
@@ -879,16 +891,16 @@ export class HomeComponent implements OnInit, OnDestroy {
}).sort((a, b) => a.timestamp - b.timestamp);
// 2. Always map and sort backend statistics
- const backendPoints = stats.statistics.map((element: number[]) => {
- const values: Record = {};
+ const backendPoints = stats.statistics.map((element: (number | null)[]) => {
+ const values: Record = {};
allLabels.forEach(labelKey => {
const labelIdx = stats.labels.indexOf(labelKey);
- values[labelKey] = labelIdx !== -1 ? element[labelIdx] : 0.0;
+ values[labelKey] = labelIdx !== -1 ? element[labelIdx] : null;
});
return {
- timestamp: Date.now() - stats.currentTimestamp + element[idxTimestamp],
- hashrate: idxHashrate !== -1 ? element[idxHashrate] : 0.0,
- power: idxPower !== -1 ? element[idxPower] : 0.0,
+ timestamp: Date.now() - stats.currentTimestamp + (element[idxTimestamp] ?? 0),
+ hashrate: (idxHashrate !== -1 ? element[idxHashrate] : 0.0) ?? 0.0,
+ power: (idxPower !== -1 ? element[idxPower] : 0.0) ?? 0.0,
values
};
}).sort((a, b) => a.timestamp - b.timestamp);
@@ -918,7 +930,7 @@ export class HomeComponent implements OnInit, OnDestroy {
if (!this.chartDatasets[labelKey]) {
this.chartDatasets[labelKey] = [];
}
- this.chartDatasets[labelKey].push(p.values[labelKey] ?? 0.0);
+ this.chartDatasets[labelKey].push(p.values[labelKey] ?? null);
});
});
}
@@ -1045,11 +1057,25 @@ export class HomeComponent implements OnInit, OnDestroy {
this.hashrateData.push(info.hashRate || 0);
this.powerData.push(info.power || 0);
+ const currentSharesAccepted = info.sharesAccepted;
+ const currentSharesRejected = info.sharesRejected;
+ const hasNewShare = (this.lastSharesAcceptedCount !== -1 && currentSharesAccepted > this.lastSharesAcceptedCount) ||
+ (this.lastSharesRejectedCount !== -1 && currentSharesRejected > this.lastSharesRejectedCount);
+
Array.from(new Set([...y1Labels, ...y2Labels])).forEach(labelKey => {
if (!this.chartDatasets[labelKey]) {
this.chartDatasets[labelKey] = [];
}
- const val = HomeComponent.getDataForLabel(chartLabelValue(labelKey) as eChartLabel, info);
+ let val: number | null;
+ if (labelKey === 'responseTimeMin') {
+ const minVal = info.responseTimeMin && info.responseTimeMin > 0 ? info.responseTimeMin : info.responseTime;
+ val = (hasNewShare && minVal > 0) ? minVal : null;
+ } else if (labelKey === 'responseTimeMax') {
+ const maxVal = info.responseTimeMax && info.responseTimeMax > 0 ? info.responseTimeMax : info.responseTime;
+ val = (hasNewShare && maxVal > 0) ? maxVal : null;
+ } else {
+ val = HomeComponent.getDataForLabel(chartLabelValue(labelKey) as eChartLabel, info);
+ }
this.chartDatasets[labelKey].push(val);
});
@@ -1110,6 +1136,12 @@ export class HomeComponent implements OnInit, OnDestroy {
formatted.temp = parseFloat(formatted.temp.toFixed(1));
formatted.temp2 = parseFloat(formatted.temp2.toFixed(1));
formatted.responseTime = parseFloat(formatted.responseTime.toFixed(1));
+ if (formatted.responseTimeMin !== undefined) {
+ formatted.responseTimeMin = parseFloat(formatted.responseTimeMin.toFixed(1));
+ }
+ if (formatted.responseTimeMax !== undefined) {
+ formatted.responseTimeMax = parseFloat(formatted.responseTimeMax.toFixed(1));
+ }
return formatted;
}),
@@ -1419,6 +1451,9 @@ export class HomeComponent implements OnInit, OnDestroy {
const y1Label = y1Labels.length > 0 ? y1Labels[0] : 'none';
const y2Label = y2Labels.length > 0 ? y2Labels[0] : 'none';
+ this.chartOptions.scales.y.min = this.getMinForLabel(chartLabelValue(y1Label) as eChartLabel);
+ this.chartOptions.scales.y2.min = this.getMinForLabel(chartLabelValue(y2Label) as eChartLabel);
+
this.chartOptions.scales.y.suggestedMax = this.getSuggestedMaxForLabel(chartLabelValue(y1Label) as eChartLabel, currentInfo);
this.chartOptions.scales.y2.suggestedMax = this.getSuggestedMaxForLabel(chartLabelValue(y2Label) as eChartLabel, currentInfo);
@@ -1482,6 +1517,11 @@ export class HomeComponent implements OnInit, OnDestroy {
}
}
+ const rMin = this.chartDatasets['responseTimeMin'];
+ const rMax = this.chartDatasets['responseTimeMax'];
+ const removedMin = rMin?.[low];
+ const removedMax = rMax?.[low];
+
// Remove point at index 'low'.
this.dataLabel.splice(low, 1);
this.hashrateData.splice(low, 1);
@@ -1489,6 +1529,20 @@ export class HomeComponent implements OnInit, OnDestroy {
Object.keys(this.chartDatasets).forEach(k => {
this.chartDatasets[k].splice(low, 1);
});
+
+ // Merge into successor (which shifted into index 'low')
+ if (rMin && removedMin != null) {
+ const currentMin = rMin[low];
+ if (currentMin == null || removedMin < currentMin) {
+ rMin[low] = removedMin;
+ }
+ }
+ if (rMax && removedMax != null) {
+ const currentMax = rMax[low];
+ if (currentMax == null || removedMax > currentMax) {
+ rMax[low] = removedMax;
+ }
+ }
}
if (this.chartData && document.visibilityState !== 'hidden') {
@@ -1512,6 +1566,14 @@ export class HomeComponent implements OnInit, OnDestroy {
}
}
+ public getMinForLabel(label: eChartLabel | undefined): number | undefined {
+ switch (label) {
+ case eChartLabel.responseTimeMin:
+ case eChartLabel.responseTimeMax: return 0;
+ default: return undefined;
+ }
+ }
+
public getSuggestedMaxForLabel(label: eChartLabel | undefined, info: ISystemInfo): number {
switch (label) {
case eChartLabel.hashrate:
@@ -1529,7 +1591,8 @@ export class HomeComponent implements OnInit, OnDestroy {
case eChartLabel.fanSpeed: return 100;
case eChartLabel.fanRpm: return 7000;
case eChartLabel.fan2Rpm: return 7000;
- case eChartLabel.responseTime: return 50;
+ case eChartLabel.responseTimeMin:
+ case eChartLabel.responseTimeMax: return 50;
default: return 0;
}
}
@@ -1553,7 +1616,8 @@ export class HomeComponent implements OnInit, OnDestroy {
case eChartLabel.fan2Rpm: return info.fan2rpm;
case eChartLabel.wifiRssi: return info.wifiRSSI;
case eChartLabel.freeHeap: return info.freeHeap;
- case eChartLabel.responseTime: return info.responseTime;
+ case eChartLabel.responseTimeMin: return info.responseTimeMin ?? info.responseTime;
+ case eChartLabel.responseTimeMax: return info.responseTimeMax ?? info.responseTime;
default: return 0.0;
}
}
@@ -1577,7 +1641,8 @@ export class HomeComponent implements OnInit, OnDestroy {
case eChartLabel.fan2Rpm: return {suffix: ' rpm', precision: 0};
case eChartLabel.wifiRssi: return {suffix: ' dBm', precision: 0};
case eChartLabel.freeHeap: return {suffix: ' B', precision: 0};
- case eChartLabel.responseTime: return {suffix: ' ms', precision: 1};
+ case eChartLabel.responseTimeMin:
+ case eChartLabel.responseTimeMax: return {suffix: ' ms', precision: 1};
default: return {suffix: '', precision: 0};
}
}
diff --git a/main/http_server/axe-os/src/app/layout/styles/layout/_theme.scss b/main/http_server/axe-os/src/app/layout/styles/layout/_theme.scss
index 2fd2b8ff43..1cb14f0b1d 100644
--- a/main/http_server/axe-os/src/app/layout/styles/layout/_theme.scss
+++ b/main/http_server/axe-os/src/app/layout/styles/layout/_theme.scss
@@ -62,6 +62,7 @@
--color-overlay-select-bg: #1a2632;
--color-overlay-select-border: #2f4562;
--color-bg-hover: rgba(255, 255, 255, 0.05);
+ --chart-axis2-color: #7bd8c0;
}
.theme-white {
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 d15e44a230..9b06dea772 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
@@ -166,6 +166,8 @@ export class SystemApiService {
fallbackStratumV2ChannelType: "extended" as const,
poolDifficulty: 1000,
responseTime: 10,
+ responseTimeMin: 10,
+ responseTimeMax: 10,
responseShareBatch: 1,
isUsingFallbackStratum: 0,
useFallbackStratum: 0,
@@ -265,7 +267,7 @@ export class SystemApiService {
const timestampData = [13131,18126,23125,28125,33125,38125,43125,48125,53125,58125];
columnList.push("timestamp");
- let statisticsList: number[][] = [];
+ let statisticsList: (number | null)[][] = [];
for(let i: number = 0; i < 10; i++) {
statisticsList[i] = [];
@@ -286,8 +288,9 @@ export class SystemApiService {
case eChartLabel.fanRpm: statisticsList[i][j] = fanRpmData[i]; break;
case eChartLabel.fan2Rpm: statisticsList[i][j] = fan2RpmData[i]; break;
case eChartLabel.wifiRssi: statisticsList[i][j] = wifiRssiData[i]; break;
- case eChartLabel.freeHeap: statisticsList[i][j] = freeHeapData[i]; break;
- case eChartLabel.responseTime: statisticsList[i][j] = responseTimeData[i]; break;
+ case eChartLabel.freeHeap: statisticsList[i][j] = freeHeapData[i]; break;
+ case eChartLabel.responseTimeMin: statisticsList[i][j] = responseTimeData[i] ? responseTimeData[i] - 1 : null; break;
+ case eChartLabel.responseTimeMax: statisticsList[i][j] = responseTimeData[i] ? responseTimeData[i] + 1 : null; break;
default:
if (columnList[j] === "timestamp") {
statisticsList[i][j] = timestampData[i];
diff --git a/main/http_server/axe-os/src/models/enum/eChartLabel.ts b/main/http_server/axe-os/src/models/enum/eChartLabel.ts
index b5ae3c06b1..1a6167a68a 100644
--- a/main/http_server/axe-os/src/models/enum/eChartLabel.ts
+++ b/main/http_server/axe-os/src/models/enum/eChartLabel.ts
@@ -16,11 +16,19 @@ export enum eChartLabel {
fan2Rpm = 'Fan 2 RPM',
wifiRssi = 'Wi-Fi RSSI',
freeHeap = 'Free Heap',
- responseTime = 'Response Time',
+ responseTimeMin = 'Response Time Min',
+ responseTimeMax = 'Response Time Max',
none = 'None'
}
-export const ChartUnitGroups: { name: string, value: string, labels: string[] }[] = [
+export interface IChartUnitGroup {
+ name: string;
+ value: string;
+ labels: string[];
+ type?: 'line' | 'scatter';
+}
+
+export const ChartUnitGroups: IChartUnitGroup[] = [
{ name: 'Hashrate (H/s)', value: 'hashrate', labels: ['hashrate', 'hashrate_1m', 'hashrate_10m', 'hashrate_1h'] },
{ name: 'Temperature (°C)', value: 'temperature', labels: ['asicTemp', 'asicTemp2', 'vrTemp'] },
{ name: 'Voltage (V)', value: 'voltage', labels: ['asicVoltage', 'voltage'] },
@@ -30,7 +38,7 @@ export const ChartUnitGroups: { name: string, value: string, labels: string[] }[
{ name: 'Percentage (%)', value: 'percentage', labels: ['fanSpeed', 'errorPercentage'] },
{ name: 'Wi-Fi RSSI (dBm)', value: 'rssi', labels: ['wifiRssi'] },
{ name: 'Free Heap (Bytes)', value: 'heap', labels: ['freeHeap'] },
- { name: 'Response Time (ms)', value: 'time', labels: ['responseTime'] },
+ { name: 'Response Time (ms)', value: 'time', labels: ['responseTimeMin', 'responseTimeMax'], type: 'scatter' },
{ name: 'None', value: 'none', labels: ['none'] }
];
diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c
index 807c8e4e70..328ffa8f04 100644
--- a/main/http_server/http_server.c
+++ b/main/http_server/http_server.c
@@ -74,7 +74,8 @@ static const char * STATS_LABEL_FAN_RPM = "fanRpm";
static const char * STATS_LABEL_FAN2_RPM = "fan2Rpm";
static const char * STATS_LABEL_WIFI_RSSI = "wifiRssi";
static const char * STATS_LABEL_FREE_HEAP = "freeHeap";
-static const char * STATS_LABEL_RESPONSE_TIME = "responseTime";
+static const char * STATS_LABEL_RESPONSE_TIME_MIN = "responseTimeMin";
+static const char * STATS_LABEL_RESPONSE_TIME_MAX = "responseTimeMax";
static int system_info_prebuffer_len = 256;
static int system_wifi_scan_prebuffer_len = 256;
@@ -99,7 +100,8 @@ typedef enum
SRC_FAN2_RPM,
SRC_WIFI_RSSI,
SRC_FREE_HEAP,
- SRC_RESPONSE_TIME,
+ SRC_RESPONSE_TIME_MIN,
+ SRC_RESPONSE_TIME_MAX,
SRC_NONE // last
} DataSource;
@@ -123,7 +125,8 @@ DataSource strToDataSource(const char * sourceStr)
if (strcmp(sourceStr, STATS_LABEL_FAN2_RPM) == 0) return SRC_FAN2_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;
- if (strcmp(sourceStr, STATS_LABEL_RESPONSE_TIME) == 0) return SRC_RESPONSE_TIME;
+ if (strcmp(sourceStr, STATS_LABEL_RESPONSE_TIME_MIN) == 0) return SRC_RESPONSE_TIME_MIN;
+ if (strcmp(sourceStr, STATS_LABEL_RESPONSE_TIME_MAX) == 0) return SRC_RESPONSE_TIME_MAX;
}
return SRC_NONE;
}
@@ -1687,7 +1690,8 @@ static esp_err_t GET_system_statistics(httpd_req_t * req)
if (dataSelection[SRC_FAN2_RPM]) { yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_FAN2_RPM); }
if (dataSelection[SRC_WIFI_RSSI]) { yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_WIFI_RSSI); }
if (dataSelection[SRC_FREE_HEAP]) { yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_FREE_HEAP); }
- if (dataSelection[SRC_RESPONSE_TIME]) { yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_RESPONSE_TIME); }
+ if (dataSelection[SRC_RESPONSE_TIME_MIN]) { yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_RESPONSE_TIME_MIN); }
+ if (dataSelection[SRC_RESPONSE_TIME_MAX]) { yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_RESPONSE_TIME_MAX); }
yyjson_mut_arr_add_str(doc, labelArray, STATS_LABEL_TIMESTAMP);
yyjson_mut_val *statsArray = yyjson_mut_obj_add_arr(doc, root, "statistics");
@@ -1713,7 +1717,20 @@ static esp_err_t GET_system_statistics(httpd_req_t * req)
if (dataSelection[SRC_FAN2_RPM]) { yyjson_mut_arr_add_uint(doc, valueArray, statsData.fan2RPM); }
if (dataSelection[SRC_WIFI_RSSI]) { yyjson_mut_arr_add_sint(doc, valueArray, statsData.wifiRSSI); }
if (dataSelection[SRC_FREE_HEAP]) { yyjson_mut_arr_add_uint(doc, valueArray, statsData.freeHeap); }
- if (dataSelection[SRC_RESPONSE_TIME]) { yyjson_mut_arr_add_real(doc, valueArray, statsData.responseTime); }
+ if (dataSelection[SRC_RESPONSE_TIME_MIN]) {
+ if (statsData.responseTimeMin > 0.0f) {
+ yyjson_mut_arr_add_real(doc, valueArray, statsData.responseTimeMin);
+ } else {
+ yyjson_mut_arr_add_null(doc, valueArray);
+ }
+ }
+ if (dataSelection[SRC_RESPONSE_TIME_MAX]) {
+ if (statsData.responseTimeMax > 0.0f) {
+ yyjson_mut_arr_add_real(doc, valueArray, statsData.responseTimeMax);
+ } else {
+ yyjson_mut_arr_add_null(doc, valueArray);
+ }
+ }
yyjson_mut_arr_add_uint(doc, valueArray, statsData.timestamp);
}
diff --git a/main/http_server/openapi.yaml b/main/http_server/openapi.yaml
index 923a6fb568..5e93fbb498 100644
--- a/main/http_server/openapi.yaml
+++ b/main/http_server/openapi.yaml
@@ -302,6 +302,8 @@ components:
- power
- resetReason
- responseTime
+ - responseTimeMin
+ - responseTimeMax
- runningPartition
- sharesAccepted
- sharesRejected
@@ -430,6 +432,12 @@ components:
responseTime:
type: number
description: Pool response time in ms
+ responseTimeMin:
+ type: number
+ description: Minimum pool response time in ms for current tick
+ responseTimeMax:
+ type: number
+ description: Maximum pool response time in ms for current tick
responseShareBatch:
type: number
description: Number of shares acknowledged in the batch that produced responseTime (SV2; 1 = single share, >1 = batched ack)
@@ -824,6 +832,7 @@ components:
description: Statistics data values(s)
items:
type: number
+ nullable: true
SystemScoreboardEntry:
type: object
@@ -1168,7 +1177,7 @@ paths:
type: array
items:
type: string
- example: [hashrate,hashrate_1m,hashrate_10m,hashrate_1h,asicTemp,vrTemp,asicVoltage,voltage,power,current,fanSpeed,fanRpm,fan2Rpm,wifiRssi,freeHeap,responseTime]
+ example: [hashrate,hashrate_1m,hashrate_10m,hashrate_1h,asicTemp,vrTemp,asicVoltage,voltage,power,current,fanSpeed,fanRpm,fan2Rpm,wifiRssi,freeHeap,responseTimeMin,responseTimeMax]
description: List of labels for which data should be retrieved
tags:
- system
diff --git a/main/http_server/system_api_json.c b/main/http_server/system_api_json.c
index 99c0582ba0..140551acb6 100644
--- a/main/http_server/system_api_json.c
+++ b/main/http_server/system_api_json.c
@@ -73,6 +73,10 @@ static void system_api_add_telemetry(cJSON *root, GlobalState *g) {
cJSON_AddNumberToObject(root, "bestSessionDiff", g->SYSTEM_MODULE.best_session_nonce_diff);
cJSON_AddNumberToObject(root, "poolDifficulty", g->SYSTEM_MODULE.pool_difficulty);
cJSON_AddFloatToObject(root, "responseTime", g->SYSTEM_MODULE.response_time);
+ float rt_min = g->SYSTEM_MODULE.response_time_min > 0.0f ? g->SYSTEM_MODULE.response_time_min : g->SYSTEM_MODULE.last_response_time_min;
+ float rt_max = g->SYSTEM_MODULE.response_time_max > 0.0f ? g->SYSTEM_MODULE.response_time_max : g->SYSTEM_MODULE.last_response_time_max;
+ cJSON_AddFloatToObject(root, "responseTimeMin", rt_min > 0.0f ? rt_min : g->SYSTEM_MODULE.response_time);
+ cJSON_AddFloatToObject(root, "responseTimeMax", rt_max > 0.0f ? rt_max : g->SYSTEM_MODULE.response_time);
cJSON_AddNumberToObject(root, "responseShareBatch", g->SYSTEM_MODULE.response_share_batch);
cJSON_AddFloatToObject(root, "processTime", g->SYSTEM_MODULE.process_time);
cJSON_AddNumberToObject(root, "workReceived", g->SYSTEM_MODULE.work_received);
diff --git a/main/system.c b/main/system.c
index 3c12e4b427..edb216b14c 100644
--- a/main/system.c
+++ b/main/system.c
@@ -518,6 +518,10 @@ void SYSTEM_reset_pool_session(GlobalState * GLOBAL_STATE)
module->shares_rejected = 0;
module->shares_pending = 0;
module->response_time = 0.0f;
+ module->response_time_min = 0.0f;
+ module->response_time_max = 0.0f;
+ module->last_response_time_min = 0.0f;
+ module->last_response_time_max = 0.0f;
module->response_share_batch = 0;
module->pool_difficulty = 0.0;
@@ -525,6 +529,20 @@ void SYSTEM_reset_pool_session(GlobalState * GLOBAL_STATE)
reset_coinbase_ui_state(GLOBAL_STATE, "");
}
+void SYSTEM_record_response_time(GlobalState * GLOBAL_STATE, float response_time_ms, uint16_t batch_count)
+{
+ if (!GLOBAL_STATE || response_time_ms <= 0.0f) return;
+ SystemModule *module = &GLOBAL_STATE->SYSTEM_MODULE;
+ module->response_time = response_time_ms;
+ module->response_share_batch = batch_count;
+ if (module->response_time_min <= 0.0f || response_time_ms < module->response_time_min) {
+ module->response_time_min = response_time_ms;
+ }
+ if (module->response_time_max <= 0.0f || response_time_ms > module->response_time_max) {
+ module->response_time_max = response_time_ms;
+ }
+}
+
void SYSTEM_decode_and_apply_coinbase(GlobalState * GLOBAL_STATE, const miner_job_t * job)
{
if (!GLOBAL_STATE || !job) return;
diff --git a/main/system.h b/main/system.h
index d45c932a0a..d03d032ead 100644
--- a/main/system.h
+++ b/main/system.h
@@ -20,6 +20,7 @@ void SYSTEM_notify_accepted_share(GlobalState * GLOBAL_STATE);
void SYSTEM_notify_rejected_share(GlobalState * GLOBAL_STATE, char * error_msg);
void SYSTEM_notify_found_nonce(GlobalState * GLOBAL_STATE, double diff, bool is_block);
void SYSTEM_notify_new_ntime(GlobalState * GLOBAL_STATE, uint32_t ntime);
+void SYSTEM_record_response_time(GlobalState * GLOBAL_STATE, float response_time_ms, uint16_t batch_count);
void SYSTEM_decode_and_apply_coinbase(GlobalState * GLOBAL_STATE, const miner_job_t * job);
diff --git a/main/tasks/statistics_task.c b/main/tasks/statistics_task.c
index 086b93284e..5d046b7d57 100644
--- a/main/tasks/statistics_task.c
+++ b/main/tasks/statistics_task.c
@@ -54,6 +54,20 @@ void removeStatisticsBuffer()
}
}
+static inline void merge_min_positive(float *target, float val)
+{
+ if (val > 0.0f && (*target <= 0.0f || val < *target)) {
+ *target = val;
+ }
+}
+
+static inline void merge_max_positive(float *target, float val)
+{
+ if (val > 0.0f && (*target <= 0.0f || val > *target)) {
+ *target = val;
+ }
+}
+
bool addStatisticData(StatisticsDataPtr data, uint16_t statsFrequency)
{
bool result = false;
@@ -113,9 +127,18 @@ bool addStatisticData(StatisticsDataPtr data, uint16_t statsFrequency)
indexToRemove = low;
}
+ struct StatisticsData *toRemove = &statisticsBuffer[indexToRemove];
+ struct StatisticsData *successor = &statisticsBuffer[indexToRemove + 1];
+
+ // Merge into successor before removing indexToRemove
+ if (indexToRemove > 0) {
+ merge_min_positive(&successor->responseTimeMin, toRemove->responseTimeMin);
+ merge_max_positive(&successor->responseTimeMax, toRemove->responseTimeMax);
+ }
+
// Shift and append (Standard linear array shift)
if (indexToRemove < maxDataCount - 1) {
- memmove(&statisticsBuffer[indexToRemove], &statisticsBuffer[indexToRemove + 1], (maxDataCount - indexToRemove - 1) * sizeof(struct StatisticsData));
+ memmove(toRemove, successor, (maxDataCount - indexToRemove - 1) * sizeof(struct StatisticsData));
}
statisticsBuffer[maxDataCount - 1] = *data;
result = true;
@@ -155,6 +178,7 @@ void statistics_task(void * pvParameters)
SystemModule * sys_module = &GLOBAL_STATE->SYSTEM_MODULE;
PowerManagementModule * power_management = &GLOBAL_STATE->POWER_MANAGEMENT_MODULE;
struct StatisticsData statsData = {};
+ uint64_t last_shares_count = sys_module->shares_accepted + sys_module->shares_rejected;
TickType_t taskWakeTime = xTaskGetTickCount();
@@ -186,7 +210,22 @@ void statistics_task(void * pvParameters)
statsData.fan2RPM = power_management->fan2_rpm;
statsData.wifiRSSI = wifiRSSI;
statsData.freeHeap = esp_get_free_heap_size();
- statsData.responseTime = sys_module->response_time;
+
+ uint64_t current_shares = sys_module->shares_accepted + sys_module->shares_rejected;
+ if (current_shares != last_shares_count && sys_module->response_time > 0.0f) {
+ statsData.responseTimeMin = sys_module->response_time_min > 0.0f ? sys_module->response_time_min : sys_module->response_time;
+ statsData.responseTimeMax = sys_module->response_time_max > 0.0f ? sys_module->response_time_max : sys_module->response_time;
+ sys_module->last_response_time_min = statsData.responseTimeMin;
+ sys_module->last_response_time_max = statsData.responseTimeMax;
+ } else {
+ statsData.responseTimeMin = 0.0f;
+ statsData.responseTimeMax = 0.0f;
+ sys_module->last_response_time_min = 0.0f;
+ sys_module->last_response_time_max = 0.0f;
+ }
+ last_shares_count = current_shares;
+ sys_module->response_time_min = 0.0f;
+ sys_module->response_time_max = 0.0f;
addStatisticData(&statsData, configStatsFrequency);
}
diff --git a/main/tasks/statistics_task.h b/main/tasks/statistics_task.h
index bb3eabc225..52bf88540a 100644
--- a/main/tasks/statistics_task.h
+++ b/main/tasks/statistics_task.h
@@ -28,7 +28,8 @@ struct StatisticsData
uint16_t fan2RPM;
int8_t wifiRSSI;
uint32_t freeHeap;
- float responseTime;
+ float responseTimeMin;
+ float responseTimeMax;
};
bool getStatisticData(uint16_t index, StatisticsDataPtr dataOut);
diff --git a/main/tasks/stratum_v1_client.c b/main/tasks/stratum_v1_client.c
index a84231dfd0..5e72dba521 100644
--- a/main/tasks/stratum_v1_client.c
+++ b/main/tasks/stratum_v1_client.c
@@ -390,10 +390,11 @@ esp_err_t stratum_v1_run(GlobalState *GLOBAL_STATE, uint16_t pool_idx)
if (s_v1_msg->response_success) {
ESP_LOGI(TAG, "message result accepted");
ESP_LOGI(TAG, "Stratum response time: %.1f ms", response_time_ms);
- GLOBAL_STATE->SYSTEM_MODULE.response_time = response_time_ms;
+ SYSTEM_record_response_time(GLOBAL_STATE, response_time_ms, 1);
SYSTEM_notify_accepted_share(GLOBAL_STATE);
} else {
ESP_LOGW(TAG, "message result rejected: %s", s_v1_msg->error_str);
+ SYSTEM_record_response_time(GLOBAL_STATE, response_time_ms, 1);
SYSTEM_notify_rejected_share(GLOBAL_STATE, s_v1_msg->error_str);
}
} else {
diff --git a/main/tasks/stratum_v2_client.c b/main/tasks/stratum_v2_client.c
index 1a72c02b39..cf2ffaa3e5 100644
--- a/main/tasks/stratum_v2_client.c
+++ b/main/tasks/stratum_v2_client.c
@@ -804,8 +804,7 @@ esp_err_t stratum_v2_run(GlobalState *GLOBAL_STATE, uint16_t pool_idx)
if (submit_time_us > 0) {
float response_time_ms = (float)(esp_timer_get_time() - submit_time_us) / 1000.0f;
ESP_LOGI(TAG, "Shares accepted: %lu (%.1f ms)", accepted_count, response_time_ms);
- GLOBAL_STATE->SYSTEM_MODULE.response_time = response_time_ms;
- GLOBAL_STATE->SYSTEM_MODULE.response_share_batch = (uint16_t)accepted_count;
+ SYSTEM_record_response_time(GLOBAL_STATE, response_time_ms, (uint16_t)accepted_count);
stratum_v2_submit_time_us[slot] = 0;
} else {
ESP_LOGI(TAG, "Shares accepted: %lu", accepted_count);