Skip to content

Commit

Permalink
Redo swarm (#430)
Browse files Browse the repository at this point in the history
* Redo swarm
Redo swarm to use network scanning and local storage rather than storing the set of IP's on device.

* auto refresh

---------

Co-authored-by: Benjamin Wilson <[email protected]>
  • Loading branch information
benjamin-wilson and Benjamin Wilson authored Nov 1, 2024
1 parent 8630fa3 commit e0090fd
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 151 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<div class="card">

<form [formGroup]="form">
<div class="field grid p-fluid">
<label htmlFor="ip" class="col-12 mb-2 md:col-2 md:mb-0">AxeOS Device IP</label>
<div class="col-12 md:col-10">
<label htmlFor="ip" class="col-12 mb-2 md:col-4 md:mb-0">Manual Addition</label>
<div class="col-12 md:col-8">
<p-inputGroup>
<input pInputText id="ip" formControlName="ip" type="text" />
<button pButton (click)="add()" [disabled]="form.invalid">Add</button>
<input pInputText id="manualAddIp" formControlName="manualAddIp" type="text" />
<button pButton [disabled]="form.invalid" (click)="add()">Add</button>
</p-inputGroup>

</div>
</div>

</form>
</div>
<button style="margin-right: 1rem;" pButton (click)="scanNetwork()" [disabled]="scanning">{{scanning ? 'Scanning...' : 'Automatic Scan'}}</button>
<button pButton severity="secondary" (click)="refreshList()" [disabled]="scanning">Refresh List ({{refreshIntervalTime}})</button>
<div>
<button pButton (click)="refresh()">Refresh</button>
</div>
<div>
<table cellspacing="0" cellpadding="0" *ngIf="swarm$ | async as swarm">
<table cellspacing="0" cellpadding="0" >
<tr>
<th>IP</th>
<th>Hash Rate</th>
Expand All @@ -31,17 +31,17 @@
<th>Restart</th>
<th>Remove</th>
</tr>
<ng-container *ngFor="let axeOs$ of swarm">
<tr *ngIf="axeOs$ | async as axe">
<td><a [href]="'http://'+axe.ip" target="_blank">{{axe.ip}}</a></td>
<ng-container *ngFor="let axe of swarm">
<tr>
<td><a [href]="'http://'+axe.IP" target="_blank">{{axe.IP}}</a></td>
<td>{{axe.hashRate * 1000000000 | hashSuffix}}</td>
<td>{{axe.uptimeSeconds | dateAgo}}</td>
<td>{{axe.sharesAccepted | number: '1.0-0'}}</td>
<td>{{axe.power | number: '1.2-2'}} <small>W</small> </td>
<td>{{axe.temp}}°<small>C</small></td>
<td>{{axe.bestDiff}}</td>
<td>{{axe.version}}</td>
<td><p-button icon="pi pi-pencil" pp-button (click)="edit(axe)"></p-button></td>
<td><p-button icon="pi pi-pencil" pp-button (click)="edit(axe)"></p-button></td>
<td><p-button icon="pi pi-sync" pp-button severity="danger" (click)="restart(axe)"></p-button></td>
<td><p-button icon="pi pi-trash" pp-button severity="secondary" (click)="remove(axe)"></p-button></td>
</tr>
Expand All @@ -52,6 +52,6 @@
<div class="modal-backdrop" *ngIf="showEdit" (click)="showEdit = false"></div>
<div class="modal card" *ngIf="showEdit">
<div class="close" (click)="showEdit = false">&#10006;</div>
<h1>{{selectedAxeOs.ip}}</h1>
<app-edit [uri]="'http://' + selectedAxeOs.ip"></app-edit>
<h1>{{selectedAxeOs.IP}}</h1>
<app-edit [uri]="'http://' + selectedAxeOs.IP"></app-edit>
</div>
204 changes: 121 additions & 83 deletions main/http_server/axe-os/src/app/components/swarm/swarm.component.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,129 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Component, OnDestroy, 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 REFRESH_TIME_SECONDS = 30;
const SWARM_DATA = 'SWARM_DATA'
@Component({
selector: 'app-swarm',
templateUrl: './swarm.component.html',
styleUrls: ['./swarm.component.scss']
})
export class SwarmComponent {

public form: FormGroup;

public swarm$: Observable<Observable<any>[]>;
export class SwarmComponent implements OnInit, OnDestroy {

public refresh$: BehaviorSubject<null> = new BehaviorSubject(null);
public swarm: any[] = [];

public selectedAxeOs: any = null;
public showEdit = false;

public form: FormGroup;

public scanning = false;

public refreshIntervalRef!: number;
public refreshIntervalTime = REFRESH_TIME_SECONDS;

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;
}

this.refreshIntervalRef = window.setInterval(() => {
this.refreshIntervalTime --;
if(this.refreshIntervalTime <= 0){
this.refreshIntervalTime = REFRESH_TIME_SECONDS;
this.refreshList();
}
}, 1000);
}

public add() {
const newIp = this.form.value.ip;

combineLatest([this.systemService.getSwarmInfo('http://' + newIp), this.systemService.getSwarmInfo()]).pipe(
switchMap(([newSwarmInfo, existingSwarmInfo]) => {
ngOnDestroy(): void {
window.clearInterval(this.refreshIntervalRef);
}

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) {
Expand All @@ -102,38 +132,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 of(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();
}
});

}

}
16 changes: 16 additions & 0 deletions main/http_server/axe-os/src/app/local-storage.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
37 changes: 37 additions & 0 deletions main/http_server/axe-os/src/app/local-storage.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit e0090fd

Please sign in to comment.