diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index d867a7b3b..6e093a02b 100755
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -28,6 +28,7 @@ SRCS
"./tasks/asic_result_task.c"
"./tasks/power_management_task.c"
"./tasks/statistics_task.c"
+ "./tasks/scoreboard.c"
"./tasks/hashrate_monitor_task.c"
"./thermal/EMC2101.c"
"./thermal/EMC2103.c"
diff --git a/main/global_state.h b/main/global_state.h
index 55f343497..43848f676 100644
--- a/main/global_state.h
+++ b/main/global_state.h
@@ -12,6 +12,7 @@
#include "work_queue.h"
#include "device_config.h"
#include "display.h"
+#include "scoreboard.h"
#define STRATUM_USER CONFIG_STRATUM_USER
#define FALLBACK_STRATUM_USER CONFIG_FALLBACK_STRATUM_USER
@@ -70,6 +71,7 @@ typedef struct
char firmware_update_filename[20];
char firmware_update_status[20];
char * asic_status;
+ Scoreboard scoreboard;
} SystemModule;
typedef struct
diff --git a/main/http_server/axe-os/src/app/app-routing.module.ts b/main/http_server/axe-os/src/app/app-routing.module.ts
index 8989ffa08..76b076e70 100644
--- a/main/http_server/axe-os/src/app/app-routing.module.ts
+++ b/main/http_server/axe-os/src/app/app-routing.module.ts
@@ -8,6 +8,7 @@ import { UpdateComponent } from './components/update/update.component';
import { SettingsComponent } from './components/settings/settings.component';
import { NetworkComponent } from './components/network/network.component';
import { SwarmComponent } from './components/swarm/swarm.component';
+import { ScoreboardComponent } from './components/scoreboard/scoreboard.component';
import { DesignComponent } from './components/design/design.component';
import { PoolComponent } from './components/pool/pool.component';
import { AppLayoutComponent } from './layout/app.layout.component';
@@ -67,6 +68,11 @@ const routes: Routes = [
component: SwarmComponent,
title: `${TITLE_PREFIX} Swarm`,
},
+ {
+ path: 'scoreboard',
+ component: ScoreboardComponent,
+ title: `${TITLE_PREFIX} Scoreboard`,
+ },
{
path: 'design',
component: DesignComponent,
diff --git a/main/http_server/axe-os/src/app/app.module.ts b/main/http_server/axe-os/src/app/app.module.ts
index c8fdd059e..7de45ffba 100644
--- a/main/http_server/axe-os/src/app/app.module.ts
+++ b/main/http_server/axe-os/src/app/app.module.ts
@@ -24,6 +24,7 @@ import { UpdateComponent } from './components/update/update.component';
import { NetworkComponent } from './components/network/network.component';
import { SettingsComponent } from './components/settings/settings.component';
import { SwarmComponent } from './components/swarm/swarm.component';
+import { ScoreboardComponent } from './components/scoreboard/scoreboard.component';
import { ThemeConfigComponent } from './components/design/theme-config.component';
import { DesignComponent } from './components/design/design.component';
import { AppLayoutModule } from './layout/app.layout.module';
@@ -62,6 +63,7 @@ const components = [
ANSIPipe,
DateAgoPipe,
SwarmComponent,
+ ScoreboardComponent,
SettingsComponent,
HashSuffixPipe,
DiffSuffixPipe,
diff --git a/main/http_server/axe-os/src/app/components/home/home.component.scss b/main/http_server/axe-os/src/app/components/home/home.component.scss
index 391fea132..b07c37f18 100644
--- a/main/http_server/axe-os/src/app/components/home/home.component.scss
+++ b/main/http_server/axe-os/src/app/components/home/home.component.scss
@@ -31,11 +31,6 @@
}
}
-.code {
- line-break: anywhere;
- font-family: 'Courier New', Courier, monospace;
-}
-
.heatmap {
width: 100%;
border-collapse: separate;
diff --git a/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.html b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.html
new file mode 100644
index 000000000..7de678662
--- /dev/null
+++ b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.html
@@ -0,0 +1,48 @@
+
+
Scoreboard
+
+
+
+
+
+
+ |
+
+ {{field.label}}
+
+
+ |
+ |
+
+
+
+
+ |
+
+ = 3" class="text-500 text-xs">{{ entry.rank + 1 }}
+ |
+ {{ entry.difficulty | diffSuffix }} |
+ {{ entry.job_id }} |
+ {{ entry.extranonce2 }} |
+ {{ entry.ntime * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} |
+ {{ entry.nonce }} |
+ {{ entry.version_bits }} |
+
+
+
+
+
+
diff --git a/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.scss b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.scss
new file mode 100644
index 000000000..a0cfb8d30
--- /dev/null
+++ b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.scss
@@ -0,0 +1,26 @@
+table {
+ border-collapse: collapse;
+}
+th,
+td {
+ padding: 0 0.5rem 0.5rem;
+ white-space: nowrap;
+ text-align: left;
+
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ }
+}
+td {
+ padding-top: 0.5rem;
+}
+tbody tr {
+ border-top: 1px solid var(--surface-border);
+
+ &:last-child td {
+ padding-bottom: 0;
+ }
+}
diff --git a/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.spec.ts b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.spec.ts
new file mode 100644
index 000000000..4cad175d0
--- /dev/null
+++ b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ScoreboardComponent } from './scoreboard.component';
+
+describe('SystemComponent', () => {
+ let component: ScoreboardComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ScoreboardComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ScoreboardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.ts b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.ts
new file mode 100644
index 000000000..077084054
--- /dev/null
+++ b/main/http_server/axe-os/src/app/components/scoreboard/scoreboard.component.ts
@@ -0,0 +1,89 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Observable, Subject, merge, switchMap, map, shareReplay, timer, takeUntil, finalize } from 'rxjs';
+import { SystemService } from 'src/app/services/system.service';
+import { LoadingService } from 'src/app/services/loading.service';
+import { LocalStorageService } from 'src/app/local-storage.service';
+import { ISystemScoreboardEntry } from 'src/models/ISystemScoreboard';
+
+const SWARM_SORTING = 'SCOREBOARD_SORTING';
+
+@Component({
+ selector: 'app-scoreboard',
+ templateUrl: './scoreboard.component.html',
+ styleUrls: ['./scoreboard.component.scss']
+})
+export class ScoreboardComponent implements OnInit, OnDestroy {
+ public scoreboard$: Observable;
+ public sortField: string = '';
+ public sortDirection: 'asc' | 'desc' = 'asc';
+
+ private refresh$ = new Subject();
+ private destroy$ = new Subject();
+
+ constructor(
+ private systemService: SystemService,
+ private loadingService: LoadingService,
+ private localStorageService: LocalStorageService,
+ ) {
+ const storedSorting = this.localStorageService.getObject(SWARM_SORTING) ?? {
+ sortField: 'rank',
+ sortDirection: 'asc'
+ };
+ this.sortField = storedSorting.sortField;
+ this.sortDirection = storedSorting.sortDirection;
+
+ this.scoreboard$ = merge(timer(0, 5000), this.refresh$).pipe(
+ switchMap(() => this.systemService.getScoreboard().pipe(
+ map(data => data.map((entry, index) => ({
+ ...entry,
+ rank: index,
+ since: (Date.now() / 1000) - entry.ntime
+ }))),
+ map(data => this.sortData(data, this.sortField, this.sortDirection)),
+ finalize(() => this.loadingService.loading$.next(false))
+ )),
+ shareReplay({refCount: true, bufferSize: 1}),
+ takeUntil(this.destroy$)
+ );
+ }
+
+ sortBy(field: string) {
+ this.sortDirection = this.sortField === field
+ ? this.sortDirection === 'asc' ? 'desc' : 'asc'
+ : 'asc';
+ this.sortField = field;
+
+ this.localStorageService.setObject(SWARM_SORTING, {
+ sortField: this.sortField,
+ sortDirection: this.sortDirection
+ });
+ this.refresh$.next();
+ }
+
+ private sortData(data: ISystemScoreboardEntry[], field: string, direction: 'asc' | 'desc'): ISystemScoreboardEntry[] {
+ if (!data || !field) {
+ return data;
+ }
+
+ return data.sort((a, b) => {
+ let valueA = a[field as keyof ISystemScoreboardEntry];
+ let valueB = b[field as keyof ISystemScoreboardEntry];
+
+ if (valueA < valueB) {
+ return direction === 'asc' ? -1 : 1;
+ } else if (valueA > valueB) {
+ return direction === 'asc' ? 1 : -1;
+ }
+ return 0;
+ });
+ }
+
+ ngOnInit() {
+ this.loadingService.loading$.next(true);
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+}
diff --git a/main/http_server/axe-os/src/app/layout/app.menu.component.ts b/main/http_server/axe-os/src/app/layout/app.menu.component.ts
index 6f6da0010..eda3104aa 100644
--- a/main/http_server/axe-os/src/app/layout/app.menu.component.ts
+++ b/main/http_server/axe-os/src/app/layout/app.menu.component.ts
@@ -25,6 +25,7 @@ export class AppMenuComponent implements OnInit {
label: 'Menu',
items: [
{ label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: ['/'] },
+ { label: 'Scoreboard', icon: 'pi pi-fw pi-trophy', routerLink: ['scoreboard'] },
{ label: 'Swarm', icon: 'pi pi-fw pi-sitemap', routerLink: ['swarm'] },
{ label: 'Logs', icon: 'pi pi-fw pi-list', routerLink: ['logs'] },
{ label: 'System', icon: 'pi pi-fw pi-wave-pulse', routerLink: ['system'] },
diff --git a/main/http_server/axe-os/src/app/pipes/date-ago.pipe.ts b/main/http_server/axe-os/src/app/pipes/date-ago.pipe.ts
index 25d751f80..d69bcaa27 100644
--- a/main/http_server/axe-os/src/app/pipes/date-ago.pipe.ts
+++ b/main/http_server/axe-os/src/app/pipes/date-ago.pipe.ts
@@ -41,12 +41,11 @@ export class DateAgoPipe implements PipeTransform {
result += counter + ' ' + i + 's'; // plural (2 days ago)
seconds -= intervals[i] * counter
}
- shownIntervals++;
}
+ if (result) shownIntervals++;
}
return result;
}
return value;
}
-
-}
\ No newline at end of file
+}
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 6c9bb6b11..8ba8fc256 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
@@ -7,6 +7,7 @@ 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';
+import { ISystemScoreboardEntry } from 'src/models/ISystemScoreboard';
import { environment } from '../../environments/environment';
@@ -179,6 +180,35 @@ export class SystemService {
});
}
+ public getScoreboard(uri: string = ''): Observable {
+ if (environment.production) {
+ return this.httpClient.get(`${uri}/api/system/scoreboard`) as Observable;
+ }
+
+ // Mock data for development
+ return of([
+ {
+ rank: 0,
+ since: 3606,
+ difficulty: 2000,
+ job_id: "123456",
+ extranonce2: "000000",
+ ntime: 61125,
+ nonce: "00000000",
+ version_bits: "20000000"
+ },
+ {
+ rank: 1,
+ since: 3605,
+ difficulty: 1000,
+ job_id: "123457",
+ extranonce2: "000001",
+ ntime: 61126,
+ nonce: "00000001",
+ version_bits: "20000000"
+ }]).pipe(delay(1000));
+ }
+
public restart(uri: string = '') {
return this.httpClient.post(`${uri}/api/system/restart`, {}, {responseType: 'text'});
}
diff --git a/main/http_server/axe-os/src/models/ISystemScoreboard.ts b/main/http_server/axe-os/src/models/ISystemScoreboard.ts
new file mode 100644
index 000000000..db2eea560
--- /dev/null
+++ b/main/http_server/axe-os/src/models/ISystemScoreboard.ts
@@ -0,0 +1,10 @@
+export interface ISystemScoreboardEntry {
+ rank: number;
+ since: number;
+ difficulty: number;
+ job_id: string;
+ extranonce2: string;
+ ntime: number;
+ nonce: string;
+ version_bits: string;
+}
diff --git a/main/http_server/axe-os/src/styles.scss b/main/http_server/axe-os/src/styles.scss
index b34234f41..a8cb7082c 100644
--- a/main/http_server/axe-os/src/styles.scss
+++ b/main/http_server/axe-os/src/styles.scss
@@ -251,6 +251,11 @@ button.color-dot {
}
}
+.code {
+ line-break: anywhere;
+ font-family: 'Courier New', Courier, monospace;
+}
+
.line-break-anywhere {
line-break: anywhere;
-}
\ No newline at end of file
+}
diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c
index 13696d85a..cf45c9752 100644
--- a/main/http_server/http_server.c
+++ b/main/http_server/http_server.c
@@ -979,6 +979,57 @@ static esp_err_t GET_system_statistics(httpd_req_t * req)
return res;
}
+static esp_err_t GET_scoreboard(httpd_req_t * req)
+{
+ if (is_network_allowed(req) != ESP_OK) {
+ return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
+ }
+
+ 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;
+ }
+
+ Scoreboard scoreboard = GLOBAL_STATE->SYSTEM_MODULE.scoreboard;
+ cJSON * root = cJSON_CreateArray();
+
+ if (xSemaphoreTake(scoreboard.mutex, portMAX_DELAY) == pdTRUE) {
+ for (int i = 0; i < scoreboard.count; i++) {
+ const ScoreboardEntry *e = &scoreboard.entries[i];
+ cJSON *entry = cJSON_CreateObject();
+
+ char nonce_str[9], version_bits_str[9];
+ snprintf(nonce_str, sizeof(nonce_str), "%08X", (unsigned int)e->nonce);
+ snprintf(version_bits_str, sizeof(version_bits_str), "%08X", (unsigned int)e->version_bits);
+
+ cJSON_AddNumberToObject(entry, "difficulty", e->difficulty);
+ cJSON_AddStringToObject(entry, "job_id", e->job_id);
+ cJSON_AddStringToObject(entry, "extranonce2", e->extranonce2);
+ cJSON_AddNumberToObject(entry, "ntime", e->ntime);
+ cJSON_AddStringToObject(entry, "nonce", nonce_str);
+ cJSON_AddStringToObject(entry, "version_bits", version_bits_str);
+
+ cJSON_AddItemToArray(root, entry);
+ }
+ xSemaphoreGive(scoreboard.mutex);
+ } else {
+ ESP_LOGE(TAG, "Failed to take mutex for JSON conversion");
+ cJSON_Delete(root);
+ return ESP_FAIL;
+ }
+
+ const char *response = cJSON_Print(root);
+ httpd_resp_sendstr(req, response);
+
+ free((void *)response);
+ cJSON_Delete(root);
+
+ return ESP_OK;
+}
+
esp_err_t POST_WWW_update(httpd_req_t * req)
{
if (is_network_allowed(req) != ESP_OK) {
@@ -1208,6 +1259,14 @@ esp_err_t start_rest_server(void * pvParameters)
};
httpd_register_uri_handler(server, &system_statistics_get_uri);
+ httpd_uri_t scoreboard_get_uri = {
+ .uri = "/api/system/scoreboard",
+ .method = HTTP_GET,
+ .handler = GET_scoreboard,
+ .user_ctx = rest_context
+ };
+ httpd_register_uri_handler(server, &scoreboard_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 55fcf99cf..2e9a1e402 100644
--- a/main/http_server/openapi.yaml
+++ b/main/http_server/openapi.yaml
@@ -705,6 +705,53 @@ paths:
'500':
description: Internal server error
+ /api/system/scoreboard:
+ get:
+ summary: Get best difficulty share scoreboard
+ description: Returns top 20 best difficulty shares
+ operationId: getSystemScoreboard
+ tags:
+ - system
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ required:
+ - difficulty
+ - job_id
+ - extranonce2
+ - ntime
+ - nonce
+ - version_bits
+ properties:
+ difficulty:
+ type: number
+ description: Difficulty of the share
+ job_id:
+ type: string
+ description: Job ID of the share
+ extranonce2:
+ type: string
+ description: Extranonce2 of the share
+ ntime:
+ type: number
+ description: Ntime of the share
+ nonce:
+ type: string
+ description: Nonce of the share
+ version_bits:
+ type: string
+ description: Version bits of the share
+ '401':
+ description: Unauthorized - Client not in allowed network range
+ '500':
+ description: Internal server error
+
/api/system/restart:
post:
summary: Restart the system
diff --git a/main/main.c b/main/main.c
index 860e04ebb..f7d9cbfb6 100644
--- a/main/main.c
+++ b/main/main.c
@@ -65,6 +65,7 @@ void app_main(void)
if (self_test(&GLOBAL_STATE)) return;
SYSTEM_init_system(&GLOBAL_STATE);
+ scoreboard_init(&GLOBAL_STATE.SYSTEM_MODULE.scoreboard);
// init AP and connect to wifi
wifi_init(&GLOBAL_STATE);
diff --git a/main/nvs_config.c b/main/nvs_config.c
index 5c6f2636f..5723e5bf3 100644
--- a/main/nvs_config.c
+++ b/main/nvs_config.c
@@ -12,6 +12,7 @@
#include
#include "display.h"
#include "theme_api.h"
+#include "scoreboard.h"
#define NVS_CONFIG_NAMESPACE "main"
#define NVS_STR_LIMIT (4000 - 1) // See nvs_set_str
@@ -35,6 +36,7 @@ typedef struct {
NvsConfigKey key;
ConfigType type;
ConfigValue value;
+ int index;
} ConfigUpdate;
static const char * TAG = "nvs_config";
@@ -81,9 +83,10 @@ static Settings settings[NVS_CONFIG_COUNT] = {
[NVS_CONFIG_BEST_DIFF] = {.nvs_key_name = "bestdiff", .type = TYPE_U64},
[NVS_CONFIG_SELF_TEST] = {.nvs_key_name = "selftest", .type = TYPE_BOOL},
- [NVS_CONFIG_SWARM] = {.nvs_key_name = "swarmconfig", .type = TYPE_STR, .default_value = {.str = ""}},
+ [NVS_CONFIG_SWARM] = {.nvs_key_name = "swarmconfig", .type = TYPE_STR},
[NVS_CONFIG_THEME_SCHEME] = {.nvs_key_name = "themescheme", .type = TYPE_STR, .default_value = {.str = DEFAULT_THEME}},
[NVS_CONFIG_THEME_COLORS] = {.nvs_key_name = "themecolors", .type = TYPE_STR, .default_value = {.str = DEFAULT_COLORS}},
+ [NVS_CONFIG_SCOREBOARD] = {.nvs_key_name = "scoreboard", .type = TYPE_STR, .array_size = MAX_SCOREBOARD},
[NVS_CONFIG_BOARD_VERSION] = {.nvs_key_name = "boardversion", .type = TYPE_STR, .default_value = {.str = "000"}},
[NVS_CONFIG_DEVICE_MODEL] = {.nvs_key_name = "devicemodel", .type = TYPE_STR, .default_value = {.str = "unknown"}},
@@ -113,6 +116,22 @@ Settings *nvs_config_get_settings(NvsConfigKey key)
return &settings[key];
}
+static int get_array_size(const Settings * setting)
+{
+ return (setting->array_size > 0) ? setting->array_size : 1;
+}
+
+static void get_nvs_key_name(const Settings * setting, const int index, char dest[static NVS_KEY_NAME_MAX_SIZE])
+{
+ if (setting->array_size > 0) {
+ int width = 1;
+ for (int t = setting->array_size - 1; t >= 10 && width < 5; t /= 10) width++;
+ snprintf(dest, NVS_KEY_NAME_MAX_SIZE, "%s_%0*d", setting->nvs_key_name, width, index + 1);
+ } else {
+ strncpy(dest, setting->nvs_key_name, NVS_KEY_NAME_MAX_SIZE);
+ }
+}
+
static void nvs_config_init_fallback(NvsConfigKey key, Settings * setting)
{
esp_err_t ret;
@@ -143,10 +162,10 @@ static void nvs_config_init_fallback(NvsConfigKey key, Settings * setting)
static void nvs_config_apply_fallback(NvsConfigKey key, Settings * setting)
{
if (key == NVS_CONFIG_ASIC_FREQUENCY) {
- nvs_set_u16(handle, FALLBACK_KEY_ASICFREQUENCY, (uint16_t) setting->value.f);
+ nvs_set_u16(handle, FALLBACK_KEY_ASICFREQUENCY, (uint16_t) setting->value[0].f);
}
if (key == NVS_CONFIG_MANUAL_FAN_SPEED) {
- nvs_set_u16(handle, FALLBACK_KEY_FANSPEED, setting->value.u16);
+ nvs_set_u16(handle, FALLBACK_KEY_FANSPEED, setting->value[0].u16);
}
}
@@ -158,34 +177,38 @@ static void nvs_task(void *pvParameters)
Settings *setting = nvs_config_get_settings(update.key);
if (setting && setting->type == update.type) {
esp_err_t ret = ESP_OK;
+
+ char key[NVS_KEY_NAME_MAX_SIZE];
+ get_nvs_key_name(setting, update.index, key);
+
char *old_str = NULL;
switch (update.type) {
case TYPE_STR:
- old_str = setting->value.str;
- setting->value.str = update.value.str;
- ret = nvs_set_str(handle, setting->nvs_key_name, setting->value.str);
+ old_str = setting->value[update.index].str;
+ setting->value[update.index].str = update.value.str;
+ ret = nvs_set_str(handle, key, setting->value[update.index].str);
break;
case TYPE_U16:
- setting->value.u16 = update.value.u16;
- ret = nvs_set_u16(handle, setting->nvs_key_name, setting->value.u16);
+ setting->value[update.index].u16 = update.value.u16;
+ ret = nvs_set_u16(handle, key, setting->value[update.index].u16);
break;
case TYPE_I32:
- setting->value.i32 = update.value.i32;
- ret = nvs_set_i32(handle, setting->nvs_key_name, setting->value.i32);
+ setting->value[update.index].i32 = update.value.i32;
+ ret = nvs_set_i32(handle, key, setting->value[update.index].i32);
break;
case TYPE_U64:
- setting->value.u64 = update.value.u64;
- ret = nvs_set_u64(handle, setting->nvs_key_name, setting->value.u64);
+ setting->value[update.index].u64 = update.value.u64;
+ ret = nvs_set_u64(handle, key, setting->value[update.index].u64);
break;
case TYPE_FLOAT:
- setting->value.f = update.value.f;
+ setting->value[update.index].f = update.value.f;
char buf[32];
- snprintf(buf, sizeof(buf), "%f", setting->value.f);
- ret = nvs_set_str(handle, setting->nvs_key_name, buf);
+ snprintf(buf, sizeof(buf), "%f", setting->value[update.index].f);
+ ret = nvs_set_str(handle, key, buf);
break;
case TYPE_BOOL:
- setting->value.b = update.value.b;
- ret = nvs_set_u16(handle, setting->nvs_key_name, setting->value.b ? 1 : 0);
+ setting->value[update.index].b = update.value.b;
+ ret = nvs_set_u16(handle, key, setting->value[update.index].b ? 1 : 0);
break;
}
@@ -219,6 +242,17 @@ esp_err_t nvs_config_init(void)
ESP_LOGW(TAG, "Could not open nvs");
return err;
}
+
+ nvs_stats_t stats;
+ err = nvs_get_stats(NULL, &stats);
+ if (err == ESP_OK) {
+ ESP_LOGI(TAG, "Used entries: %lu", stats.used_entries);
+ ESP_LOGI(TAG, "Free entries: %lu", stats.free_entries);
+ ESP_LOGI(TAG, "Available entries: %lu", stats.available_entries);
+ ESP_LOGI(TAG, "Total entries: %lu", stats.total_entries);
+ } else {
+ ESP_LOGE(TAG, "Error getting NVS stats: %s\n", esp_err_to_name(err));
+ }
// Load all
for (NvsConfigKey key = 0; key < NVS_CONFIG_COUNT; key++) {
@@ -227,50 +261,65 @@ esp_err_t nvs_config_init(void)
nvs_config_init_fallback(key, setting);
esp_err_t ret;
- switch (setting->type) {
- case TYPE_STR: {
- size_t len = 0;
- nvs_get_str(handle, setting->nvs_key_name, NULL, &len);
- char *buf = len > 0 ? malloc(len) : NULL;
- if (buf) {
- ret = nvs_get_str(handle, setting->nvs_key_name, buf, &len);
- setting->value.str = (ret == ESP_OK) ? buf : strdup(setting->default_value.str);
- if (ret != ESP_OK) free(buf);
- } else {
- setting->value.str = strdup(setting->default_value.str);
+
+ int count = get_array_size(setting);
+ setting->value = calloc(count, sizeof(ConfigValue));
+
+ for (int idx = 0; idx < count; idx++) {
+ char key[NVS_KEY_NAME_MAX_SIZE];
+ get_nvs_key_name(setting, idx, key);
+
+ switch (setting->type) {
+ case TYPE_STR: {
+ size_t len = 0;
+ esp_err_t ret = nvs_get_str(handle, key, NULL, &len);
+ if (ret == ESP_OK && len > 1) {
+ char *buf = malloc(len);
+ if (buf) {
+ ret = nvs_get_str(handle, key, buf, &len);
+ if (ret == ESP_OK) {
+ setting->value[idx].str = buf;
+ break;
+ }
+ free(buf);
+ }
+ }
+
+ const char *def = setting->default_value.str ? setting->default_value.str : "";
+ setting->value[idx].str = strdup(def);
+ break;
+ }
+ case TYPE_U16: {
+ uint16_t val;
+ ret = nvs_get_u16(handle, key, &val);
+ setting->value[idx].u16 = (ret == ESP_OK) ? val : setting->default_value.u16;
+ break;
+ }
+ case TYPE_I32: {
+ int32_t val;
+ ret = nvs_get_i32(handle, key, &val);
+ setting->value[idx].i32 = (ret == ESP_OK) ? val : setting->default_value.i32;
+ break;
+ }
+ case TYPE_U64: {
+ uint64_t val;
+ ret = nvs_get_u64(handle, key, &val);
+ setting->value[idx].u64 = (ret == ESP_OK) ? val : setting->default_value.u64;
+ break;
+ }
+ case TYPE_FLOAT: {
+ char buf[32];
+ size_t len = sizeof(buf);
+ ret = nvs_get_str(handle, key, buf, &len);
+ setting->value[idx].f = (ret == ESP_OK) ? atof(buf) : setting->default_value.f;
+ break;
+ }
+ case TYPE_BOOL: {
+ uint16_t val;
+ ret = nvs_get_u16(handle, key, &val);
+ setting->value[idx].b = (ret == ESP_OK) ? (val != 0) : setting->default_value.b;
+ break;
}
- break;
- }
- case TYPE_U16: {
- uint16_t val;
- ret = nvs_get_u16(handle, setting->nvs_key_name, &val);
- setting->value.u16 = (ret == ESP_OK) ? val : setting->default_value.u16;
- break;
- }
- case TYPE_I32: {
- int32_t val;
- ret = nvs_get_i32(handle, setting->nvs_key_name, &val);
- setting->value.i32 = (ret == ESP_OK) ? val : setting->default_value.i32;
- break;
- }
- case TYPE_U64: {
- uint64_t val;
- ret = nvs_get_u64(handle, setting->nvs_key_name, &val);
- setting->value.u64 = (ret == ESP_OK) ? val : setting->default_value.u64;
- break;
- }
- case TYPE_FLOAT: {
- char buf[32];
- size_t len = sizeof(buf);
- ret = nvs_get_str(handle, setting->nvs_key_name, buf, &len);
- setting->value.f = (ret == ESP_OK) ? atof(buf) : setting->default_value.f;
- break;
- }
- case TYPE_BOOL: {
- uint16_t val;
- ret = nvs_get_u16(handle, setting->nvs_key_name, &val);
- setting->value.b = (ret == ESP_OK) ? (val != 0) : setting->default_value.b;
- break;
}
}
}
@@ -292,11 +341,20 @@ esp_err_t nvs_config_init(void)
char *nvs_config_get_string(NvsConfigKey key)
{
Settings *setting = nvs_config_get_settings(key);
- if (!setting || setting->type != TYPE_STR) {
+ if (!setting || setting->type != TYPE_STR || setting->array_size > 1) {
ESP_LOGE(TAG, "Wrong type for %s (str)", setting->nvs_key_name);
return NULL;
}
- return strdup(setting->value.str);
+ return strdup(setting->value[0].str);
+}
+
+char *nvs_config_get_string_indexed(NvsConfigKey key, int index)
+{
+ Settings *setting = nvs_config_get_settings(key);
+ if (!setting || setting->type != TYPE_STR || setting->array_size < 1 || index < 0 || index >= setting->array_size) {
+ return NULL;
+ }
+ return strdup(setting->value[index].str);
}
void nvs_config_set_string(NvsConfigKey key, const char *value)
@@ -306,6 +364,13 @@ void nvs_config_set_string(NvsConfigKey key, const char *value)
xQueueSend(nvs_save_queue, &update, portMAX_DELAY);
}
+void nvs_config_set_string_indexed(NvsConfigKey key, int index, const char *value)
+{
+ ConfigUpdate update = { .key = key, .type = TYPE_STR, .value.str = strdup(value), .index = index };
+ if (!update.value.str) return;
+ xQueueSend(nvs_save_queue, &update, portMAX_DELAY);
+}
+
uint16_t nvs_config_get_u16(NvsConfigKey key)
{
Settings *setting = nvs_config_get_settings(key);
@@ -313,7 +378,7 @@ uint16_t nvs_config_get_u16(NvsConfigKey key)
ESP_LOGE(TAG, "Wrong type for %s (u16)", setting->nvs_key_name);
return 0;
}
- return setting->value.u16;
+ return setting->value[0].u16;
}
void nvs_config_set_u16(NvsConfigKey key, uint16_t value)
@@ -329,7 +394,7 @@ int32_t nvs_config_get_i32(NvsConfigKey key)
ESP_LOGE(TAG, "Wrong type for %s (i32)", setting->nvs_key_name);
return 0;
}
- return setting->value.i32;
+ return setting->value[0].i32;
}
void nvs_config_set_i32(NvsConfigKey key, int32_t value)
@@ -345,7 +410,7 @@ uint64_t nvs_config_get_u64(NvsConfigKey key)
ESP_LOGE(TAG, "Wrong type for %s (u64)", setting->nvs_key_name);
return 0;
}
- return setting->value.u64;
+ return setting->value[0].u64;
}
void nvs_config_set_u64(NvsConfigKey key, uint64_t value)
@@ -361,7 +426,7 @@ float nvs_config_get_float(NvsConfigKey key)
ESP_LOGE(TAG, "Wrong type for %s (float)", setting->nvs_key_name);
return 0;
}
- return setting->value.f;
+ return setting->value[0].f;
}
void nvs_config_set_float(NvsConfigKey key, float value)
@@ -378,7 +443,7 @@ bool nvs_config_get_bool(NvsConfigKey key)
ESP_LOGE(TAG, "Wrong type for %s (bool)", setting->nvs_key_name);
return false;
}
- return setting->value.b;
+ return setting->value[0].b;
}
void nvs_config_set_bool(NvsConfigKey key, bool value)
diff --git a/main/nvs_config.h b/main/nvs_config.h
index 91ffeefbb..dc7a3b681 100644
--- a/main/nvs_config.h
+++ b/main/nvs_config.h
@@ -47,6 +47,7 @@ typedef enum {
NVS_CONFIG_SWARM,
NVS_CONFIG_THEME_SCHEME,
NVS_CONFIG_THEME_COLORS,
+ NVS_CONFIG_SCOREBOARD,
NVS_CONFIG_BOARD_VERSION,
NVS_CONFIG_DEVICE_MODEL,
@@ -90,7 +91,8 @@ typedef union {
typedef struct {
const char *nvs_key_name;
ConfigType type;
- ConfigValue value;
+ ConfigValue *value;
+ int array_size; // Numbered entries
ConfigValue default_value;
const char *rest_name;
int min;
@@ -100,7 +102,9 @@ typedef struct {
esp_err_t nvs_config_init(void);
char * nvs_config_get_string(NvsConfigKey key);
+char *nvs_config_get_string_indexed(NvsConfigKey key, int index);
void nvs_config_set_string(NvsConfigKey key, const char * value);
+void nvs_config_set_string_indexed(NvsConfigKey key, int index, const char *value);
uint16_t nvs_config_get_u16(NvsConfigKey key);
void nvs_config_set_u16(NvsConfigKey key, uint16_t value);
int32_t nvs_config_get_i32(NvsConfigKey key);
diff --git a/main/tasks/asic_result_task.c b/main/tasks/asic_result_task.c
index 17ef449c9..20e415dcb 100644
--- a/main/tasks/asic_result_task.c
+++ b/main/tasks/asic_result_task.c
@@ -10,6 +10,7 @@
#include "stratum_task.h"
#include "hashrate_monitor_task.h"
#include "asic.h"
+#include "scoreboard.h"
static const char *TAG = "asic_result";
@@ -53,6 +54,7 @@ void ASIC_result_task(void *pvParameters)
//log the ASIC response
ESP_LOGI(TAG, "ID: %s, ASIC nr: %d, ver: %08" PRIX32 " Nonce %08" PRIX32 " diff %.1f of %ld.", active_job->jobid, asic_result->asic_nr, asic_result->rolled_version, asic_result->nonce, nonce_diff, active_job->pool_diff);
+ uint32_t version_bits = asic_result->rolled_version ^ active_job->version;
if (nonce_diff >= active_job->pool_diff)
{
char * user = GLOBAL_STATE->SYSTEM_MODULE.is_using_fallback ? GLOBAL_STATE->SYSTEM_MODULE.fallback_pool_user : GLOBAL_STATE->SYSTEM_MODULE.pool_user;
@@ -64,7 +66,7 @@ void ASIC_result_task(void *pvParameters)
active_job->extranonce2,
active_job->ntime,
asic_result->nonce,
- asic_result->rolled_version ^ active_job->version);
+ version_bits);
if (ret < 0) {
ESP_LOGI(TAG, "Unable to write share to socket. Closing connection. Ret: %d (errno %d: %s)", ret, errno, strerror(errno));
@@ -73,5 +75,7 @@ void ASIC_result_task(void *pvParameters)
}
SYSTEM_notify_found_nonce(GLOBAL_STATE, nonce_diff, job_id);
+
+ scoreboard_add(&GLOBAL_STATE->SYSTEM_MODULE.scoreboard, nonce_diff, active_job->jobid, active_job->extranonce2, active_job->ntime, asic_result->nonce, version_bits);
}
}
diff --git a/main/tasks/scoreboard.c b/main/tasks/scoreboard.c
new file mode 100644
index 000000000..ec50540c2
--- /dev/null
+++ b/main/tasks/scoreboard.c
@@ -0,0 +1,91 @@
+#include "scoreboard.h"
+#include "nvs_config.h"
+#include "esp_log.h"
+#include
+
+static const char * TAG = "scoreboard";
+
+void scoreboard_init(Scoreboard *scoreboard) {
+ scoreboard->count = 0;
+ scoreboard->mutex = xSemaphoreCreateMutex();
+ if (scoreboard->mutex == NULL) {
+ ESP_LOGE(TAG, "Failed to create mutex");
+ }
+
+ for (int i = 0; i < MAX_SCOREBOARD; i++) {
+ char *entry_str = nvs_config_get_string_indexed(NVS_CONFIG_SCOREBOARD, i);
+ if (entry_str[0] == '\0') {
+ free(entry_str);
+ break;
+ }
+
+ ScoreboardEntry entry;
+ if (sscanf(entry_str, "%lf;%31[^;];%31[^;];%lu;%lu;%lu",
+ &entry.difficulty,
+ entry.job_id,
+ entry.extranonce2,
+ &entry.ntime,
+ &entry.nonce,
+ &entry.version_bits) == 6) {
+ strncpy(entry.nvs_entry, entry_str, sizeof(entry.nvs_entry) - 1);
+ entry.nvs_entry[sizeof(entry.nvs_entry) - 1] = '\0';
+ scoreboard->entries[scoreboard->count++] = entry;
+ } else {
+ ESP_LOGW(TAG, "Failed to parse scoreboard entry from NVS: %s", entry_str);
+ }
+ free(entry_str);
+ }
+}
+
+static void scoreboard_save(int i, ScoreboardEntry *entry) {
+ nvs_config_set_string_indexed(NVS_CONFIG_SCOREBOARD, i, entry->nvs_entry);
+}
+
+void scoreboard_add(Scoreboard *scoreboard, double difficulty, const char *job_id, const char *extranonce2, uint32_t ntime, uint32_t nonce, uint32_t version_bits)
+{
+ int i = (scoreboard->count < MAX_SCOREBOARD) ? scoreboard->count : MAX_SCOREBOARD - 1;
+
+ if (scoreboard->count == MAX_SCOREBOARD && i >= 0 && difficulty <= scoreboard->entries[i].difficulty) {
+ return;
+ }
+
+ ScoreboardEntry new_entry = {
+ .difficulty = difficulty,
+ .ntime = ntime,
+ .nonce = nonce,
+ .version_bits = version_bits,
+ };
+ strncpy(new_entry.job_id, job_id, sizeof(new_entry.job_id) - 1);
+ new_entry.job_id[sizeof(new_entry.job_id) - 1] = '\0';
+ strncpy(new_entry.extranonce2, extranonce2, sizeof(new_entry.extranonce2) - 1);
+ new_entry.extranonce2[sizeof(new_entry.extranonce2) - 1] = '\0';
+ snprintf(new_entry.nvs_entry, sizeof(new_entry.nvs_entry),
+ "%.1f;%s;%s;%lu;%lu;%lu",
+ new_entry.difficulty,
+ new_entry.job_id,
+ new_entry.extranonce2,
+ new_entry.ntime,
+ new_entry.nonce,
+ new_entry.version_bits);
+
+ if (xSemaphoreTake(scoreboard->mutex, portMAX_DELAY) == pdTRUE) {
+ while (i > 0 && difficulty > scoreboard->entries[i - 1].difficulty) {
+ scoreboard->entries[i] = scoreboard->entries[i - 1];
+ scoreboard_save(i, &scoreboard->entries[i]);
+ i--;
+ }
+
+ scoreboard->entries[i] = new_entry;
+ scoreboard_save(i, &new_entry);
+ if (scoreboard->count < MAX_SCOREBOARD) {
+ scoreboard->count++;
+ }
+ xSemaphoreGive(scoreboard->mutex);
+ } else {
+ ESP_LOGE(TAG, "Failed to take mutex");
+ return;
+ }
+
+ ESP_LOGI(TAG, "New #%d: Difficulty: %.1f, Job ID: %s, extranonce2: %s, ntime: %d, nonce: %08X, version_bits: %08X",
+ i+1, new_entry.difficulty, new_entry.job_id, new_entry.extranonce2, new_entry.ntime, (unsigned int)new_entry.nonce, (unsigned int)new_entry.version_bits);
+}
diff --git a/main/tasks/scoreboard.h b/main/tasks/scoreboard.h
new file mode 100644
index 000000000..60c22e652
--- /dev/null
+++ b/main/tasks/scoreboard.h
@@ -0,0 +1,33 @@
+#ifndef SCOREBOARD_H
+#define SCOREBOARD_H
+
+#include
+#include
+#include
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/semphr.h"
+#include "esp_err.h"
+
+#define MAX_SCOREBOARD 20
+
+typedef struct {
+ double difficulty;
+ char job_id[32];
+ char extranonce2[32];
+ uint32_t ntime;
+ uint32_t nonce;
+ uint32_t version_bits;
+ char nvs_entry[128];
+} ScoreboardEntry;
+
+typedef struct {
+ ScoreboardEntry entries[MAX_SCOREBOARD];
+ int count;
+ SemaphoreHandle_t mutex;
+} Scoreboard;
+
+void scoreboard_init(Scoreboard *scoreboard);
+void scoreboard_add(Scoreboard *scoreboard, double difficulty, const char *job_id, const char *extranonce2, uint32_t ntime, uint32_t nonce, uint32_t version_bits);
+
+#endif /* SCOREBOARD_H */
diff --git a/readme.md b/readme.md
index 85be050ca..52eec3f68 100755
--- a/readme.md
+++ b/readme.md
@@ -59,6 +59,7 @@ Available API endpoints:
* `/api/system/asic` Get ASIC settings information
* `/api/system/statistics` Get system statistics (data logging should be activated)
* `/api/system/statistics/dashboard` Get system statistics for dashboard
+* `/api/system/scoreboard` Get top 20 highest difficulty shares
* `/api/system/wifi/scan` Scan for available Wi-Fi networks
**POST**