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}} + +
+
+ + {{ 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**