Skip to content

Commit 6ffc023

Browse files
committed
feat: Enhance swarm component to support hostname addresses and improve device discovery
1 parent 00bb9a1 commit 6ffc023

File tree

3 files changed

+125
-54
lines changed

3 files changed

+125
-54
lines changed

main/http_server/axe-os/src/app/components/swarm/swarm.component.html

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
<form [formGroup]="form">
44
<div class="field grid p-fluid mb-0">
5-
<label htmlFor="ip" class="col-12 mb-2 md:col-4 md:mb-0">Manual Addition</label>
5+
<label htmlFor="manualAddAddress" class="col-12 mb-2 md:col-4 md:mb-0">Manual Addition</label>
66
<div class="col-12 md:col-8">
77
<p-inputGroup>
8-
<input pInputText id="manualAddIp" formControlName="manualAddIp" type="text" />
8+
<input pInputText id="manualAddAddress" formControlName="manualAddAddress" type="text" placeholder="192.168.1.100 or bitaxe.local" />
99
<button pButton [disabled]="form.invalid" (click)="add()">Add</button>
1010
</p-inputGroup>
1111

@@ -64,8 +64,8 @@
6464
<tr>
6565
<th *ngFor="let field of [
6666
{
67-
name: 'IP',
68-
label: 'IP'
67+
name: 'address',
68+
label: 'Address'
6969
},
7070
{
7171
name: 'hostname',
@@ -121,11 +121,11 @@
121121
<tr>
122122
<td>
123123
<a
124-
[ngClass]="'text-'+axe.swarmColor+'-500'"
125-
[href]="'http://'+axe.IP"
126-
target="_blank"
127-
[pTooltip]="axe.deviceModel || 'Other'"
128-
tooltipPosition="top">{{axe.IP}}</a>
124+
[ngClass]="'text-'+axe.swarmColor+'-500'"
125+
[href]="'http://'+axe.address"
126+
target="_blank"
127+
[pTooltip]="axe.deviceModel || 'Other'"
128+
tooltipPosition="top">{{axe.address}}</a>
129129
</td>
130130
<td>{{axe.hostname}}</td>
131131
<td>{{axe.hashRate * 1000000000 | hashSuffix}}</td>
@@ -188,6 +188,6 @@
188188

189189
</div>
190190

191-
<app-modal [headline]="selectedAxeOs?.IP">
192-
<app-edit *ngIf="selectedAxeOs" [uri]="'http://' + selectedAxeOs.IP"></app-edit>
191+
<app-modal [headline]="selectedAxeOs?.address">
192+
<app-edit *ngIf="selectedAxeOs" [uri]="'http://' + selectedAxeOs.address"></app-edit>
193193
</app-modal>

main/http_server/axe-os/src/app/components/swarm/swarm.component.ts

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const SWARM_DATA = 'SWARM_DATA';
1010
const SWARM_REFRESH_TIME = 'SWARM_REFRESH_TIME';
1111
const SWARM_SORTING = 'SWARM_SORTING';
1212

13-
type SwarmDevice = { IP: string; ASICModel: string; deviceModel: string; swarmColor: string; asicCount: number; [key: string]: any };
13+
type SwarmDevice = { address: string; ASICModel: string; deviceModel: string; swarmColor: string; asicCount: number; [key: string]: any };
1414

1515
@Component({
1616
selector: 'app-swarm',
@@ -50,7 +50,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
5050
) {
5151

5252
this.form = this.fb.group({
53-
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]?)')]]
53+
manualAddAddress: [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]?)|[a-zA-Z0-9-]+\\.local$')]]
5454
});
5555

5656
const storedRefreshTime = this.localStorageService.getNumber(SWARM_REFRESH_TIME) ?? 30;
@@ -65,7 +65,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
6565
});
6666

6767
const storedSorting = this.localStorageService.getObject(SWARM_SORTING) ?? {
68-
sortField: 'IP',
68+
sortField: 'address',
6969
sortDirection: 'asc'
7070
};
7171
this.sortField = storedSorting.sortField;
@@ -101,6 +101,11 @@ export class SwarmComponent implements OnInit, OnDestroy {
101101
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
102102
}
103103

104+
private isIpAddress(value: string): boolean {
105+
const ipRegex = /^(?:(?: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]?)$/;
106+
return ipRegex.test(value);
107+
}
108+
104109
private intToIp(int: number): string {
105110
return `${(int >>> 24) & 255}.${(int >>> 16) & 255}.${(int >>> 8) & 255}.${int & 255}`;
106111
}
@@ -116,15 +121,37 @@ export class SwarmComponent implements OnInit, OnDestroy {
116121
scanNetwork() {
117122
this.scanning = true;
118123

119-
const { start, end } = this.calculateIpRange(window.location.hostname, '255.255.255.0');
120-
const ips = Array.from({ length: end - start + 1 }, (_, i) => this.intToIp(start + i));
124+
if (this.isIpAddress(window.location.hostname)) {
125+
// Direct IP access - scan the subnet
126+
const { start, end } = this.calculateIpRange(window.location.hostname, '255.255.255.0');
127+
const ips = Array.from({ length: end - start + 1 }, (_, i) => this.intToIp(start + i));
128+
this.performNetworkScan(ips);
129+
} else {
130+
// mDNS hostname - fetch server IP first, then scan its subnet
131+
this.httpClient.get(`http://${window.location.hostname}/api/system/info`)
132+
.subscribe({
133+
next: (response: any) => {
134+
const serverIp = response.ip;
135+
const { start, end } = this.calculateIpRange(serverIp, '255.255.255.0');
136+
const ips = Array.from({ length: end - start + 1 }, (_, i) => this.intToIp(start + i));
137+
this.performNetworkScan(ips);
138+
},
139+
error: () => {
140+
// Fallback: skip scanning if we can't get the IP
141+
this.scanning = false;
142+
}
143+
});
144+
}
145+
}
146+
147+
private performNetworkScan(ips: string[]) {
121148
this.getAllDeviceInfo(ips, () => of(null)).subscribe({
122149
next: (result) => {
123150
// Filter out null items first
124151
const validResults = result.filter((item): item is SwarmDevice => item !== null);
125152
// Merge new results with existing swarm entries
126-
const existingIps = new Set(this.swarm.map(item => item.IP));
127-
const newItems = validResults.filter(item => !existingIps.has(item.IP));
153+
const existingAddresses = new Set(this.swarm.map(item => item.address));
154+
const newItems = validResults.filter(item => !existingAddresses.has(item.address));
128155
this.swarm = [...this.swarm, ...newItems];
129156
this.sortSwarm();
130157
this.localStorageService.setObject(SWARM_DATA, this.swarm);
@@ -136,19 +163,31 @@ export class SwarmComponent implements OnInit, OnDestroy {
136163
});
137164
}
138165

139-
private getAllDeviceInfo(ips: string[], errorHandler: (error: any, ip: string) => Observable<SwarmDevice[] | null>, fetchAsic: boolean = true) {
140-
return from(ips).pipe(
141-
mergeMap(IP => forkJoin({
142-
info: this.httpClient.get(`http://${IP}/api/system/info`),
143-
asic: fetchAsic ? this.httpClient.get(`http://${IP}/api/system/asic`).pipe(catchError(() => of({}))) : of({})
166+
private getAllDeviceInfo(addresses: string[], errorHandler: (error: any, address: string) => Observable<SwarmDevice[] | null>, fetchAsic: boolean = true) {
167+
return from(addresses).pipe(
168+
mergeMap(address => forkJoin({
169+
info: this.httpClient.get(`http://${address}/api/system/info`).pipe(catchError(error => {
170+
if (error.status === 401 || error.status === 0) {
171+
// Show warning for potential older device or CORS blocked (likely 401)
172+
this.toastr.warning(`Potential swarm peer detected at ${address} - upgrade its firmware to be able to add it.`);
173+
return of({ _corsError: 401 });
174+
}
175+
throw error;
176+
})),
177+
asic: fetchAsic ? this.httpClient.get(`http://${address}/api/system/asic`).pipe(catchError(() => of({}))) : of({})
144178
}).pipe(
145179
map(({ info, asic }) => {
146-
const existingDevice = this.swarm.find(device => device.IP === IP);
147-
const result = { IP, ...(existingDevice ? existingDevice : {}), ...info, ...asic };
180+
// Skip processing if we already showed the warning
181+
if ((info as any)._corsError === 401) {
182+
return null;
183+
}
184+
185+
const existingDevice = this.swarm.find(device => device.address === address);
186+
const result = { address, ...(existingDevice ? existingDevice : {}), ...info, ...asic };
148187
return this.fallbackDeviceModel(result);
149188
}),
150189
timeout(5000),
151-
catchError(error => errorHandler(error, IP))
190+
catchError(error => errorHandler(error, address))
152191
),
153192
128
154193
),
@@ -157,61 +196,70 @@ export class SwarmComponent implements OnInit, OnDestroy {
157196
}
158197

159198
public add() {
160-
const IP = this.form.value.manualAddIp;
199+
const address = this.form.value.manualAddAddress;
161200

162-
// Check if IP already exists
163-
if (this.swarm.some(item => item.IP === IP)) {
164-
this.toastr.warning('This IP address already exists in the swarm.');
201+
// Check if address already exists
202+
if (this.swarm.some(item => item.address === address)) {
203+
this.toastr.warning('This address already exists in the swarm.');
165204
return;
166205
}
167206

168207
forkJoin({
169-
info: this.httpClient.get<any>(`http://${IP}/api/system/info`),
170-
asic: this.httpClient.get<any>(`http://${IP}/api/system/asic`).pipe(catchError(() => of({})))
208+
info: this.httpClient.get<any>(`http://${address}/api/system/info`).pipe(catchError(error => {
209+
if (error.status === 401 || error.status === 0) {
210+
this.toastr.warning(`Potential swarm peer detected at ${address} - upgrade its firmware to be able to add it.`);
211+
return of({ _corsError: 401 });
212+
}
213+
throw error;
214+
})),
215+
asic: this.httpClient.get<any>(`http://${address}/api/system/asic`).pipe(catchError(() => of({})))
171216
}).subscribe(({ info, asic }) => {
217+
if ((info as any)._corsError === 401) {
218+
return; // Already showed warning
219+
}
172220
if (!info.ASICModel || !asic.ASICModel) {
173221
return;
174222
}
175223

176-
this.swarm.push({ IP, ...asic, ...info });
224+
this.swarm.push({ address, ...asic, ...info });
177225
this.sortSwarm();
178226
this.localStorageService.setObject(SWARM_DATA, this.swarm);
179227
this.calculateTotals();
180228
});
181229
}
182230

183-
public edit(axe: any) {
184-
this.selectedAxeOs = axe;
231+
public edit(device: any) {
232+
this.selectedAxeOs = device;
185233
this.modalComponent.isVisible = true;
186234
}
187235

188-
public restart(axe: any) {
189-
this.httpClient.post(`http://${axe.IP}/api/system/restart`, {}).pipe(
236+
public restart(device: any) {
237+
this.httpClient.post(`http://${device.address}/api/system/restart`, {}).pipe(
190238
catchError(error => {
191239
if (error.status === 0 || error.status === 200 || error.name === 'HttpErrorResponse') {
192240
return of('success');
193241
} else {
194-
this.toastr.error(`Failed to restart device at ${axe.IP}`);
242+
this.toastr.error(`Failed to restart device at ${device.address}`);
195243
return of(null);
196244
}
197245
})
198246
).subscribe(res => {
199247
if (res !== null && res == 'success') {
200-
this.toastr.success(`Device at ${axe.IP} restarted`);
248+
this.toastr.success(`Device at ${device.address} restarted`);
201249
}
202250
});
203251
}
204252

205-
public remove(axeOs: any) {
206-
this.swarm = this.swarm.filter(axe => axe.IP !== axeOs.IP);
253+
public remove(device: any) {
254+
this.swarm = this.swarm.filter(axe => axe.address !== device.address);
207255
this.localStorageService.setObject(SWARM_DATA, this.swarm);
208256
this.calculateTotals();
209257
}
210258

211-
public refreshErrorHandler = (error: any, ip: string) => {
259+
public refreshErrorHandler = (error: any, address: string) => {
212260
const errorMessage = error?.message || error?.statusText || error?.toString() || 'Unknown error';
213-
this.toastr.error(`Failed to get info from ${ip}`);
214-
const existingDevice = this.swarm.find(axeOs => axeOs.IP === ip);
261+
this.toastr.error(`Failed to get info from ${address}`);
262+
const existingDevice = this.swarm.find(axeOs => axeOs.address === address);
215263
return of({
216264
...existingDevice,
217265
hashRate: 0,
@@ -232,10 +280,10 @@ export class SwarmComponent implements OnInit, OnDestroy {
232280
}
233281

234282
this.refreshIntervalTime = this.refreshTimeSet;
235-
const ips = this.swarm.map(axeOs => axeOs.IP);
283+
const addresses = this.swarm.map(axeOs => axeOs.address);
236284
this.isRefreshing = true;
237285

238-
this.getAllDeviceInfo(ips, this.refreshErrorHandler, fetchAsic).subscribe({
286+
this.getAllDeviceInfo(addresses, this.refreshErrorHandler, fetchAsic).subscribe({
239287
next: (result) => {
240288
this.swarm = result;
241289
this.sortSwarm();
@@ -272,15 +320,28 @@ export class SwarmComponent implements OnInit, OnDestroy {
272320
let comparison = 0;
273321
const fieldType = typeof a[this.sortField];
274322

275-
if (this.sortField === 'IP') {
276-
// Split IP into octets and compare numerically
277-
const aOctets = a[this.sortField].split('.').map(Number);
278-
const bOctets = b[this.sortField].split('.').map(Number);
279-
for (let i = 0; i < 4; i++) {
280-
if (aOctets[i] !== bOctets[i]) {
281-
comparison = aOctets[i] - bOctets[i];
282-
break;
323+
if (this.sortField === 'address') {
324+
const aValue = a[this.sortField];
325+
const bValue = b[this.sortField];
326+
const aIsIp = this.isIpAddress(aValue);
327+
const bIsIp = this.isIpAddress(bValue);
328+
329+
if (aIsIp && bIsIp) {
330+
// Both are IPs, sort numerically
331+
const aOctets = aValue.split('.').map(Number);
332+
const bOctets = bValue.split('.').map(Number);
333+
for (let i = 0; i < 4; i++) {
334+
if (aOctets[i] !== bOctets[i]) {
335+
comparison = aOctets[i] - bOctets[i];
336+
break;
337+
}
283338
}
339+
} else if (!aIsIp && !bIsIp) {
340+
// Both are hostnames, sort alphabetically
341+
comparison = aValue.localeCompare(bValue);
342+
} else {
343+
// Mixed, sort IPs before hostnames
344+
comparison = aIsIp ? -1 : 1;
284345
}
285346
} else if (fieldType === 'number') {
286347
comparison = a[this.sortField] - b[this.sortField];

main/http_server/http_server.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,15 @@ static esp_err_t GET_system_info(httpd_req_t * req)
748748
int8_t wifi_rssi = -90;
749749
get_wifi_current_rssi(&wifi_rssi);
750750

751+
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
752+
char ip_str[16] = "";
753+
if (netif != NULL) {
754+
esp_netif_ip_info_t ip_info;
755+
if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
756+
inet_ntop(AF_INET, &ip_info.ip, ip_str, sizeof(ip_str));
757+
}
758+
}
759+
751760
cJSON * root = cJSON_CreateObject();
752761
cJSON_AddNumberToObject(root, "power", GLOBAL_STATE->POWER_MANAGEMENT_MODULE.power);
753762
cJSON_AddNumberToObject(root, "voltage", GLOBAL_STATE->POWER_MANAGEMENT_MODULE.voltage);
@@ -776,6 +785,7 @@ static esp_err_t GET_system_info(httpd_req_t * req)
776785
cJSON_AddStringToObject(root, "hostname", hostname);
777786
cJSON_AddStringToObject(root, "wifiStatus", GLOBAL_STATE->SYSTEM_MODULE.wifi_status);
778787
cJSON_AddNumberToObject(root, "wifiRSSI", wifi_rssi);
788+
cJSON_AddStringToObject(root, "ip", ip_str);
779789
cJSON_AddNumberToObject(root, "apEnabled", GLOBAL_STATE->SYSTEM_MODULE.ap_enabled);
780790
cJSON_AddNumberToObject(root, "sharesAccepted", GLOBAL_STATE->SYSTEM_MODULE.shares_accepted);
781791
cJSON_AddNumberToObject(root, "sharesRejected", GLOBAL_STATE->SYSTEM_MODULE.shares_rejected);

0 commit comments

Comments
 (0)