Skip to content

Commit 30f4f2c

Browse files
committed
init
1 parent 33dff51 commit 30f4f2c

File tree

10 files changed

+791
-27
lines changed

10 files changed

+791
-27
lines changed

main/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ SRCS
1111
"lv_font_portfolio-6x8.c"
1212
"logo.c"
1313
"./http_server/http_server.c"
14+
"./http_server/handler_ota_github.c"
1415
"./self_test/self_test.c"
1516
"./tasks/stratum_task.c"
1617
"./tasks/create_jobs_task.c"
@@ -47,6 +48,7 @@ PRIV_REQUIRES
4748
"esp_adc"
4849
"esp_app_format"
4950
"esp_event"
51+
"esp_http_client"
5052
"esp_http_server"
5153
"esp_netif"
5254
"esp_psram"

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

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,56 @@ <h2>Settings</h2>
44
</div>
55

66
<div class="grid">
7-
<div class="col-12 lg:col-6 xl:col-4">
7+
<div class="col-12 lg:col-6">
88
<div class="card" *ngIf="checkLatestRelease == false">
99
<h5>Current Version: {{(info$ | async)?.version}}</h5>
10-
<h2>Latest Release: <p-button (onClick)="checkLatestRelease = true">Check</p-button></h2>
10+
<h2>Release & Update: <p-button (onClick)="loadReleases()">Check</p-button></h2>
1111
<small>Clicking this button will connect to GitHub to check for recent updates</small>
1212
</div>
1313
<div class="card" *ngIf="checkLatestRelease == true">
14-
<ng-container *ngIf="latestRelease$ | async as latestRelease">
1514
<h5>Current Version: {{(info$ | async)?.version}}</h5>
16-
<h2>Latest Release: {{latestRelease.name}}</h2>
1715

18-
<div *ngFor="let asset of latestRelease.assets">
19-
<div *ngIf="asset.name == 'bitforgeos.bin'">
20-
<a style="text-decoration: underline;" target="_blank"
21-
[href]="asset.browser_download_url">bitforgeos.bin</a>
16+
<ng-container *ngIf="releases$ | async as releases">
17+
<div class="mb-3">
18+
<label class="block mb-2 font-bold">Select Release</label>
19+
<p-dropdown
20+
[options]="releases"
21+
optionLabel="name"
22+
placeholder="Select a release"
23+
(onChange)="onReleaseSelected($event.value)"
24+
[style]="{'width': '100%'}">
25+
</p-dropdown>
2226
</div>
23-
<div *ngIf="asset.name == 'www.bin'">
24-
<a style="text-decoration: underline;" target="_blank"
25-
[href]="asset.browser_download_url">www.bin</a>
27+
28+
<div *ngIf="selectedRelease" class="mb-3">
29+
<div *ngFor="let asset of selectedRelease.assets">
30+
<div *ngIf="asset.name == 'bitforgeos.bin' || asset.name == 'www.bin'">
31+
<a style="text-decoration: underline;" target="_blank"
32+
[href]="asset.browser_download_url">{{asset.name}}</a>
33+
<small class="ml-2">({{(asset.size / 1024 / 1024).toFixed(1)}} MB)</small>
34+
</div>
35+
</div>
36+
37+
<div class="mt-3">
38+
<p-button
39+
label="Install from GitHub"
40+
icon="pi pi-download"
41+
[disabled]="isGithubOTA"
42+
(onClick)="installFromGithub()">
43+
</p-button>
44+
</div>
45+
46+
<div *ngIf="isGithubOTA" class="mt-3">
47+
<div class="mb-2">
48+
<strong>{{getStepLabel(githubOTAStep)}}</strong>
49+
</div>
50+
<p-progressBar [value]="githubOTAProgress"></p-progressBar>
51+
</div>
2652
</div>
27-
</div>
28-
</ng-container>
53+
</ng-container>
2954
</div>
3055
</div>
31-
<div class="col-12 lg:col-6 xl:col-4">
56+
<div class="col-12 lg:col-6 xl:col-3">
3257
<div class="card">
3358
<h2>Update Website <span *ngIf="websiteUpdateProgress != null">{{websiteUpdateProgress}}%</span></h2>
3459

@@ -38,10 +63,9 @@ <h2>Update Website <span *ngIf="websiteUpdateProgress != null">{{websiteUpdatePr
3863
<small>(www.bin)</small>
3964
</div>
4065
</div>
41-
<div class="col-12 lg:col-6 xl:col-4">
66+
<div class="col-12 lg:col-6 xl:col-3">
4267
<div class="card">
4368
<h2>Update Firmware <span *ngIf="firmwareUpdateProgress != null">{{firmwareUpdateProgress}}%</span></h2>
44-
<!-- <input type="file" id="file" (change)="otaUpdate($event)" accept=".bin"> -->
4569
<p-fileUpload #firmwareUpload [customUpload]="true" mode="basic" accept=".bin" (uploadHandler)="otaUpdate($event)"
4670
[auto]="true" chooseLabel="Browse"></p-fileUpload>
4771

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

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { HttpErrorResponse, HttpEventType } from '@angular/common/http';
2-
import { Component, ViewChild } from '@angular/core';
2+
import { Component, OnDestroy, ViewChild } from '@angular/core';
33
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
44
import { ToastrService } from 'ngx-toastr';
55
import { FileUploadHandlerEvent, FileUpload } from 'primeng/fileupload';
6-
import { map, Observable, shareReplay, startWith } from 'rxjs';
7-
import { GithubUpdateService } from 'src/app/services/github-update.service';
6+
import { map, Observable, shareReplay, startWith, Subscription } from 'rxjs';
7+
import { GithubRelease, GithubUpdateService } from 'src/app/services/github-update.service';
88
import { LoadingService } from 'src/app/services/loading.service';
99
import { SystemService } from 'src/app/services/system.service';
1010
import { eASICModel } from 'src/models/enum/eASICModel';
@@ -14,7 +14,7 @@ import { eASICModel } from 'src/models/enum/eASICModel';
1414
templateUrl: './settings.component.html',
1515
styleUrls: ['./settings.component.scss']
1616
})
17-
export class SettingsComponent {
17+
export class SettingsComponent implements OnDestroy {
1818

1919
public form!: FormGroup;
2020

@@ -27,6 +27,15 @@ export class SettingsComponent {
2727

2828
public checkLatestRelease: boolean = false;
2929
public latestRelease$: Observable<any>;
30+
public releases$!: Observable<GithubRelease[]>;
31+
public selectedRelease: GithubRelease | null = null;
32+
33+
// GitHub OTA state
34+
public isGithubOTA: boolean = false;
35+
public githubOTAStep: string = 'idle';
36+
public githubOTAProgress: number = 0;
37+
private otaPollSub: Subscription | null = null;
38+
private rebootCheckSub: Subscription | null = null;
3039

3140
public info$: Observable<any>;
3241

@@ -86,6 +95,14 @@ export class SettingsComponent {
8695
});
8796

8897
}
98+
99+
ngOnDestroy() {
100+
this.stopOTAPolling();
101+
if (this.rebootCheckSub) {
102+
this.rebootCheckSub.unsubscribe();
103+
}
104+
}
105+
89106
public updateSystem() {
90107

91108
const form = this.form.getRawValue();
@@ -191,6 +208,132 @@ export class SettingsComponent {
191208
});
192209
}
193210

211+
public loadReleases() {
212+
this.checkLatestRelease = true;
213+
this.releases$ = this.githubUpdateService.getReleases();
214+
}
215+
216+
public onReleaseSelected(release: GithubRelease) {
217+
this.selectedRelease = release;
218+
}
219+
220+
public getStepLabel(step: string): string {
221+
switch (step) {
222+
case 'downloading_fw': return 'Downloading firmware...';
223+
case 'flashing_fw': return 'Downloading & flashing firmware...';
224+
case 'downloading_www': return 'Downloading website...';
225+
case 'flashing_www': return 'Flashing website...';
226+
case 'rebooting': return 'Rebooting...';
227+
case 'error': return 'Error';
228+
default: return 'Starting...';
229+
}
230+
}
231+
232+
public installFromGithub() {
233+
if (!this.selectedRelease) return;
234+
235+
const fwUrl = this.githubUpdateService.findAssetUrl(this.selectedRelease, 'bitforgeos.bin');
236+
const wwwUrl = this.githubUpdateService.findAssetUrl(this.selectedRelease, 'www.bin');
237+
238+
if (!fwUrl || !wwwUrl) {
239+
this.toastrService.error('Release is missing bitforgeos.bin or www.bin assets', 'Error');
240+
return;
241+
}
242+
243+
this.isGithubOTA = true;
244+
this.githubOTAStep = 'idle';
245+
this.githubOTAProgress = 0;
246+
247+
this.systemService.startGithubOTA(fwUrl, wwwUrl).subscribe({
248+
next: () => {
249+
this.toastrService.info('Update started', 'GitHub OTA');
250+
this.startOTAPolling();
251+
},
252+
error: (err) => {
253+
this.isGithubOTA = false;
254+
const msg = err.error?.message || err.message || 'Failed to start update';
255+
this.toastrService.error(msg, 'Error');
256+
}
257+
});
258+
}
259+
260+
private startOTAPolling() {
261+
this.stopOTAPolling();
262+
263+
const poll = () => {
264+
this.otaPollSub = this.systemService.getGithubOTAStatus().subscribe({
265+
next: (status) => {
266+
this.githubOTAStep = status.step;
267+
this.githubOTAProgress = status.progress;
268+
269+
if (status.step === 'rebooting') {
270+
this.stopOTAPolling();
271+
this.toastrService.success('Update complete! Device is rebooting...', 'Success');
272+
this.startRebootCheck();
273+
return;
274+
}
275+
276+
if (status.step === 'error') {
277+
this.stopOTAPolling();
278+
this.isGithubOTA = false;
279+
this.toastrService.error(status.error || 'Update failed', 'Error');
280+
return;
281+
}
282+
283+
if (status.running) {
284+
setTimeout(() => poll(), 1000);
285+
} else {
286+
this.isGithubOTA = false;
287+
}
288+
},
289+
error: () => {
290+
// Device may have rebooted, start checking
291+
this.stopOTAPolling();
292+
this.startRebootCheck();
293+
}
294+
});
295+
};
296+
297+
poll();
298+
}
299+
300+
private stopOTAPolling() {
301+
if (this.otaPollSub) {
302+
this.otaPollSub.unsubscribe();
303+
this.otaPollSub = null;
304+
}
305+
}
306+
307+
private startRebootCheck() {
308+
let attempts = 0;
309+
const maxAttempts = 60;
310+
311+
const check = () => {
312+
attempts++;
313+
if (attempts > maxAttempts) {
314+
this.isGithubOTA = false;
315+
this.toastrService.warning('Device did not come back online within 60 seconds', 'Warning');
316+
return;
317+
}
318+
319+
this.rebootCheckSub = this.systemService.getInfo().subscribe({
320+
next: () => {
321+
// Device is back!
322+
this.isGithubOTA = false;
323+
this.toastrService.success('Device is back online! Reloading...', 'Success');
324+
setTimeout(() => window.location.reload(), 1500);
325+
},
326+
error: () => {
327+
// Not back yet, retry
328+
setTimeout(() => check(), 1000);
329+
}
330+
});
331+
};
332+
333+
// Wait 5 seconds before first check
334+
setTimeout(() => check(), 5000);
335+
}
336+
194337
public restart() {
195338
this.systemService.restart().subscribe(res => {
196339

main/http_server/axe-os/src/app/prime-ng.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FileUploadModule } from 'primeng/fileupload';
77
import { InputGroupModule } from 'primeng/inputgroup';
88
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
99
import { InputTextModule } from 'primeng/inputtext';
10+
import { ProgressBarModule } from 'primeng/progressbar';
1011
import { SidebarModule } from 'primeng/sidebar';
1112
import { SliderModule } from 'primeng/slider';
1213

@@ -21,6 +22,7 @@ const primeNgModules = [
2122
ChartModule,
2223
InputGroupModule,
2324
InputGroupAddonModule,
25+
ProgressBarModule,
2426
];
2527

2628
@NgModule({

main/http_server/axe-os/src/app/services/github-update.service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ import { Observable } from 'rxjs';
44
import { map } from 'rxjs/operators';
55

66

7-
interface GithubRelease {
7+
export interface GithubAsset {
8+
name: string;
9+
browser_download_url: string;
10+
size: number;
11+
}
12+
13+
export interface GithubRelease {
814
id: number;
915
tag_name: string;
1016
name: string;
1117
prerelease: boolean;
18+
body: string;
19+
assets: GithubAsset[];
1220
}
1321

1422
@Injectable({
@@ -29,4 +37,8 @@ export class GithubUpdateService {
2937
);
3038
}
3139

40+
public findAssetUrl(release: GithubRelease, filename: string): string | null {
41+
const asset = release.assets.find(a => a.name === filename);
42+
return asset ? asset.browser_download_url : null;
43+
}
3244
}

main/http_server/axe-os/src/app/services/system.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ export class SystemService {
121121
}
122122

123123

124+
public startGithubOTA(fwUrl: string, wwwUrl: string): Observable<any> {
125+
return this.httpClient.post('/api/system/OTA/github', { fw_url: fwUrl, www_url: wwwUrl });
126+
}
127+
128+
public getGithubOTAStatus(): Observable<any> {
129+
return this.httpClient.get('/api/system/OTA/github');
130+
}
131+
124132
public getSwarmInfo(uri: string = ''): Observable<{ ip: string }[]> {
125133
return this.httpClient.get(`${uri}/api/swarm/info`) as Observable<{ ip: string }[]>;
126134
}

0 commit comments

Comments
 (0)