From a28771062aa62d4a72a7c2fbe98eea0218af0deb Mon Sep 17 00:00:00 2001 From: Benjamin Wilson Date: Sun, 27 Oct 2024 21:12:56 -0400 Subject: [PATCH 1/2] Redo swarm Redo swarm to use network scanning and local storage rather than storing the set of IP's on device. --- .../app/components/swarm/swarm.component.html | 28 +-- .../app/components/swarm/swarm.component.ts | 189 ++++++++++-------- .../src/app/local-storage.service.spec.ts | 16 ++ .../axe-os/src/app/local-storage.service.ts | 37 ++++ main/http_server/http_server.c | 54 ----- 5 files changed, 173 insertions(+), 151 deletions(-) create mode 100644 main/http_server/axe-os/src/app/local-storage.service.spec.ts create mode 100644 main/http_server/axe-os/src/app/local-storage.service.ts diff --git a/main/http_server/axe-os/src/app/components/swarm/swarm.component.html b/main/http_server/axe-os/src/app/components/swarm/swarm.component.html index e8973c217..34e31f227 100644 --- a/main/http_server/axe-os/src/app/components/swarm/swarm.component.html +++ b/main/http_server/axe-os/src/app/components/swarm/swarm.component.html @@ -1,11 +1,12 @@
+
- -
+ +
- - + +
@@ -13,11 +14,10 @@
+ +
- -
-
- +
@@ -31,9 +31,9 @@ - - - + + + @@ -41,7 +41,7 @@ - + @@ -52,6 +52,6 @@ \ No newline at end of file diff --git a/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts b/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts index ea51f16e1..104f4f8cc 100644 --- a/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts +++ b/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts @@ -1,99 +1,114 @@ -import { Component } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; -import { BehaviorSubject, catchError, combineLatest, forkJoin, map, Observable, of, startWith, switchMap } from 'rxjs'; +import { BehaviorSubject, catchError, combineLatest, debounce, debounceTime, forkJoin, from, interval, map, mergeAll, mergeMap, Observable, of, startWith, switchMap, take, timeout, toArray } from 'rxjs'; +import { LocalStorageService } from 'src/app/local-storage.service'; import { SystemService } from 'src/app/services/system.service'; +const SWARM_DATA = 'SWARM_DATA' @Component({ selector: 'app-swarm', templateUrl: './swarm.component.html', styleUrls: ['./swarm.component.scss'] }) -export class SwarmComponent { +export class SwarmComponent implements OnInit { - public form: FormGroup; - - public swarm$: Observable[]>; - - public refresh$: BehaviorSubject = new BehaviorSubject(null); + public swarm: any[] = []; public selectedAxeOs: any = null; public showEdit = false; + public form: FormGroup; + + public scanning = false; + constructor( private fb: FormBuilder, private systemService: SystemService, - private toastr: ToastrService + private toastr: ToastrService, + private localStorageService: LocalStorageService, + private httpClient: HttpClient ) { - this.form = this.fb.group({ - ip: [null, [Validators.required, Validators.pattern('(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)')]] - }); - - this.swarm$ = this.systemService.getSwarmInfo().pipe( - map(swarmInfo => { - return swarmInfo.map(({ ip }) => { - // Make individual API calls for each IP - return this.refresh$.pipe( - switchMap(() => { - return this.systemService.getInfo(`http://${ip}`); - }) - ).pipe( - startWith({ ip }), - map(info => { - return { - ip, - ...info - }; - }), - catchError(error => { - return of({ ip, error: true }); - }) - ); - }); - }) - ); + this.form = this.fb.group({ + manualAddIp: [null, [Validators.required, Validators.pattern('(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)')]] + }) } + ngOnInit(): void { + const swarmData = this.localStorageService.getObject(SWARM_DATA); + console.log(swarmData); + if (swarmData == null) { + this.scanNetwork(); + //this.swarm$ = this.scanNetwork('192.168.1.23', '255.255.255.0').pipe(take(1)); + } else { + this.swarm = swarmData; + } + } - public add() { - const newIp = this.form.value.ip; - - combineLatest([this.systemService.getSwarmInfo('http://' + newIp), this.systemService.getSwarmInfo()]).pipe( - switchMap(([newSwarmInfo, existingSwarmInfo]) => { - - if (existingSwarmInfo.length < 1) { - existingSwarmInfo.push({ ip: window.location.host }); - } - const swarmUpdate = existingSwarmInfo.map(({ ip }) => { - return this.systemService.updateSwarm('http://' + ip, [{ ip: newIp }, ...newSwarmInfo, ...existingSwarmInfo]) - }); + private ipToInt(ip: string): number { + return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; + } - const newAxeOs = this.systemService.updateSwarm('http://' + newIp, [{ ip: newIp }, ...existingSwarmInfo]) + private intToIp(int: number): string { + return `${(int >>> 24) & 255}.${(int >>> 16) & 255}.${(int >>> 8) & 255}.${int & 255}`; + } - return forkJoin([newAxeOs, ...swarmUpdate]); + private calculateIpRange(ip: string, netmask: string): { start: number, end: number } { + const ipInt = this.ipToInt(ip); + const netmaskInt = this.ipToInt(netmask); + const network = ipInt & netmaskInt; + const broadcast = network | ~netmaskInt; + return { start: network + 1, end: broadcast - 1 }; + } - }) - ).subscribe({ - next: () => { - this.toastr.success('Success!', 'Saved.'); - window.location.reload(); - }, - error: (err) => { - this.toastr.error('Error.', `Could not save. ${err.message}`); + scanNetwork() { + this.scanning = true; + + const { start, end } = this.calculateIpRange(window.location.hostname, '255.255.255.0'); + const ips = Array.from({ length: end - start + 1 }, (_, i) => this.intToIp(start + i)); + from(ips).pipe( + mergeMap(ipAddr => + this.httpClient.get(`http://${ipAddr}/api/system/info`).pipe( + map(result => { + return { + IP: ipAddr, + ...result + } + }), + timeout(5000), // Set the timeout to 1 second + catchError(error => { + //console.error(`Request to ${ipAddr}/api/system/info failed or timed out`, error); + return []; // Return an empty result or handle as desired + }) + ), + 256 // Limit concurrency to avoid overload + ), + toArray() // Collect all results into a single array + ).pipe(take(1)).subscribe({ + next: (result) => { + this.swarm = result; + this.localStorageService.setObject(SWARM_DATA, this.swarm); }, complete: () => { - this.form.reset(); + this.scanning = false; } }); - } - public refresh() { - this.refresh$.next(null); + public add() { + const newIp = this.form.value.manualAddIp; + + this.systemService.getInfo(`http://${newIp}`).subscribe((res) => { + if (res.ASICModel) { + this.swarm.push({ IP: newIp, ...res }); + this.localStorageService.setObject(SWARM_DATA, this.swarm); + } + }); } public edit(axe: any) { @@ -102,38 +117,46 @@ export class SwarmComponent { } public restart(axe: any) { - this.systemService.restart(`http://${axe.ip}`).subscribe(res => { + this.systemService.restart(`http://${axe.IP}`).subscribe(res => { }); this.toastr.success('Success!', 'Bitaxe restarted'); } public remove(axeOs: any) { - this.systemService.getSwarmInfo().pipe( - switchMap((swarmInfo) => { - - const newSwarm = swarmInfo.filter((s: any) => s.ip != axeOs.ip); - - const swarmUpdate = newSwarm.map(({ ip }) => { - return this.systemService.updateSwarm('http://' + ip, newSwarm) - }); - - const removedAxeOs = this.systemService.updateSwarm('http://' + axeOs.ip, []); + this.swarm = this.swarm.filter(axe => axe.IP != axeOs.IP); + this.localStorageService.setObject(SWARM_DATA, this.swarm); + } - return forkJoin([removedAxeOs, ...swarmUpdate]); - }) - ).subscribe({ - next: () => { - this.toastr.success('Success!', 'Saved.'); - window.location.reload(); - }, - error: (err) => { - this.toastr.error('Error.', `Could not save. ${err.message}`); + public refreshList() { + const ips = this.swarm.map(axeOs => axeOs.IP); + + from(ips).pipe( + mergeMap(ipAddr => + this.httpClient.get(`http://${ipAddr}/api/system/info`).pipe( + map(result => { + return { + IP: ipAddr, + ...result + } + }), + timeout(5000), + catchError(error => { + return this.swarm.find(axeOs => axeOs.IP == ipAddr); + }) + ), + 256 // Limit concurrency to avoid overload + ), + toArray() // Collect all results into a single array + ).pipe(take(1)).subscribe({ + next: (result) => { + this.swarm = result; + this.localStorageService.setObject(SWARM_DATA, this.swarm); }, complete: () => { - this.form.reset(); } }); + } } diff --git a/main/http_server/axe-os/src/app/local-storage.service.spec.ts b/main/http_server/axe-os/src/app/local-storage.service.spec.ts new file mode 100644 index 000000000..ba1dbd436 --- /dev/null +++ b/main/http_server/axe-os/src/app/local-storage.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LocalStorageService } from './local-storage.service'; + +describe('LocalStorageService', () => { + let service: LocalStorageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LocalStorageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/main/http_server/axe-os/src/app/local-storage.service.ts b/main/http_server/axe-os/src/app/local-storage.service.ts new file mode 100644 index 000000000..adeb596db --- /dev/null +++ b/main/http_server/axe-os/src/app/local-storage.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LocalStorageService { + + constructor() { } + + setItem(key: string, value: any) { + localStorage.setItem(key, value); + } + + getItem(key: string): any { + return localStorage.getItem(key); + } + + setBool(key: string, value: boolean) { + localStorage.setItem(key, String(value)); + } + + getBool(key: string): boolean { + return localStorage.getItem(key) === 'true'; + } + + setObject(key: string, value: object) { + localStorage.setItem(key, JSON.stringify(value)); + } + + getObject(key: string): any | null{ + const item = localStorage.getItem(key); + if(item == null || item.length < 1){ + return null; + } + return JSON.parse(item); + } +} diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c index 2c5afb9ac..04db4aa2b 100644 --- a/main/http_server/http_server.c +++ b/main/http_server/http_server.c @@ -193,38 +193,6 @@ static esp_err_t rest_common_get_handler(httpd_req_t * req) return ESP_OK; } -static esp_err_t PATCH_update_swarm(httpd_req_t * req) -{ - // Set CORS headers - if (set_cors_headers(req) != ESP_OK) { - httpd_resp_send_500(req); - return ESP_FAIL; - } - - int total_len = req->content_len; - int cur_len = 0; - char * buf = ((rest_server_context_t *) (req->user_ctx))->scratch; - int received = 0; - if (total_len >= SCRATCH_BUFSIZE) { - /* Respond with 500 Internal Server Error */ - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long"); - return ESP_FAIL; - } - while (cur_len < total_len) { - received = httpd_req_recv(req, buf + cur_len, total_len); - if (received <= 0) { - /* Respond with 500 Internal Server Error */ - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value"); - return ESP_FAIL; - } - cur_len += received; - } - buf[total_len] = '\0'; - - nvs_config_set_string(NVS_CONFIG_SWARM, buf); - httpd_resp_send_chunk(req, NULL, 0); - return ESP_OK; -} static esp_err_t handle_options_request(httpd_req_t * req) { @@ -351,21 +319,6 @@ static esp_err_t POST_restart(httpd_req_t * req) return ESP_OK; } -static esp_err_t GET_swarm(httpd_req_t * req) -{ - httpd_resp_set_type(req, "application/json"); - - // Set CORS headers - if (set_cors_headers(req) != ESP_OK) { - httpd_resp_send_500(req); - return ESP_FAIL; - } - - char * swarm_config = nvs_config_get_string(NVS_CONFIG_SWARM, "[]"); - httpd_resp_sendstr(req, swarm_config); - free(swarm_config); - return ESP_OK; -} /* Simple handler for getting system handler */ static esp_err_t GET_system_info(httpd_req_t * req) @@ -729,13 +682,6 @@ esp_err_t start_rest_server(void * pvParameters) .uri = "/api/system/info", .method = HTTP_GET, .handler = GET_system_info, .user_ctx = rest_context}; httpd_register_uri_handler(server, &system_info_get_uri); - httpd_uri_t swarm_get_uri = {.uri = "/api/swarm/info", .method = HTTP_GET, .handler = GET_swarm, .user_ctx = rest_context}; - httpd_register_uri_handler(server, &swarm_get_uri); - - httpd_uri_t update_swarm_uri = { - .uri = "/api/swarm", .method = HTTP_PATCH, .handler = PATCH_update_swarm, .user_ctx = rest_context}; - httpd_register_uri_handler(server, &update_swarm_uri); - httpd_uri_t swarm_options_uri = { .uri = "/api/swarm", .method = HTTP_OPTIONS, From 0112b6c3be57ac20261074d06daa0de1799a7db6 Mon Sep 17 00:00:00 2001 From: Benjamin Wilson Date: Fri, 1 Nov 2024 17:27:40 -0400 Subject: [PATCH 2/2] auto refresh --- .../app/components/swarm/swarm.component.html | 2 +- .../app/components/swarm/swarm.component.ts | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/main/http_server/axe-os/src/app/components/swarm/swarm.component.html b/main/http_server/axe-os/src/app/components/swarm/swarm.component.html index 34e31f227..8340595b0 100644 --- a/main/http_server/axe-os/src/app/components/swarm/swarm.component.html +++ b/main/http_server/axe-os/src/app/components/swarm/swarm.component.html @@ -15,7 +15,7 @@ - +
IP Hash RateRestart Remove
{{axe.ip}}
{{axe.IP}} {{axe.hashRate * 1000000000 | hashSuffix}} {{axe.uptimeSeconds | dateAgo}} {{axe.sharesAccepted | number: '1.0-0'}}{{axe.temp}}°C {{axe.bestDiff}} {{axe.version}}
diff --git a/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts b/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts index 104f4f8cc..5df3254e0 100644 --- a/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts +++ b/main/http_server/axe-os/src/app/components/swarm/swarm.component.ts @@ -1,18 +1,18 @@ import { HttpClient } from '@angular/common/http'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; import { BehaviorSubject, catchError, combineLatest, debounce, debounceTime, forkJoin, from, interval, map, mergeAll, mergeMap, Observable, of, startWith, switchMap, take, timeout, toArray } from 'rxjs'; import { LocalStorageService } from 'src/app/local-storage.service'; import { SystemService } from 'src/app/services/system.service'; - +const REFRESH_TIME_SECONDS = 30; const SWARM_DATA = 'SWARM_DATA' @Component({ selector: 'app-swarm', templateUrl: './swarm.component.html', styleUrls: ['./swarm.component.scss'] }) -export class SwarmComponent implements OnInit { +export class SwarmComponent implements OnInit, OnDestroy { public swarm: any[] = []; @@ -23,6 +23,9 @@ export class SwarmComponent implements OnInit { public scanning = false; + public refreshIntervalRef!: number; + public refreshIntervalTime = REFRESH_TIME_SECONDS; + constructor( private fb: FormBuilder, private systemService: SystemService, @@ -36,6 +39,7 @@ export class SwarmComponent implements OnInit { }) } + ngOnInit(): void { const swarmData = this.localStorageService.getObject(SWARM_DATA); console.log(swarmData); @@ -45,8 +49,19 @@ export class SwarmComponent implements OnInit { } else { this.swarm = swarmData; } + + this.refreshIntervalRef = window.setInterval(() => { + this.refreshIntervalTime --; + if(this.refreshIntervalTime <= 0){ + this.refreshIntervalTime = REFRESH_TIME_SECONDS; + this.refreshList(); + } + }, 1000); } + ngOnDestroy(): void { + window.clearInterval(this.refreshIntervalRef); + } @@ -142,7 +157,7 @@ export class SwarmComponent implements OnInit { }), timeout(5000), catchError(error => { - return this.swarm.find(axeOs => axeOs.IP == ipAddr); + return of(this.swarm.find(axeOs => axeOs.IP == ipAddr)); }) ), 256 // Limit concurrency to avoid overload