Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
55695cb
feat: add pause/resume functionality
b-rowan Mar 13, 2026
2df44d4
fix(build): add java to devcontainer so build doesn't fail when updat…
b-rowan Mar 15, 2026
02bfda2
chore(refactor): add unified handler for reading request bodies in we…
b-rowan Mar 16, 2026
8ddd613
chore(refactor): move frequency reset to mining_stop
b-rowan Mar 17, 2026
f492cec
Merge branch 'master' into pause-resume
b-rowan Mar 17, 2026
b789bca
chore(refactor): split pause and resume endpoints
b-rowan Mar 17, 2026
4f2e45b
Revert "fix(build): add java to devcontainer so build doesn't fail wh…
b-rowan Mar 18, 2026
646c92d
chore(lint): fix unrelated change
b-rowan Mar 18, 2026
e8b34c9
chore(icons): swap to circle icons
b-rowan Mar 18, 2026
d2cdf6b
chore(refactor): use generic response and inline pause/resume into po…
b-rowan Mar 18, 2026
93fa0d3
chore(refactor): use message directly from backend to create toast me…
b-rowan Mar 18, 2026
dcca989
chore(logs): prevent spamming the logs when mining is paused
b-rowan Mar 18, 2026
6997b12
fix(fan): set fan speed to 30% when mining is paused
b-rowan Mar 18, 2026
8d28832
fix(ui): add more information about mining state to swarm page
b-rowan Mar 18, 2026
0620077
fix(pool): stop pool stratum task when mining is stopped
b-rowan Mar 19, 2026
672bfa4
Revert "chore(refactor): add unified handler for reading request bodi…
b-rowan Mar 19, 2026
e95b68e
chore: remove frequency reset function
b-rowan Mar 19, 2026
40d032a
feat: add label for pause state on the screen
b-rowan Mar 19, 2026
7113c4f
chore: fix PR comments
b-rowan Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions main/global_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ typedef struct
bool is_using_fallback;
char pool_connection_info[64];
bool overheat_mode;
bool mining_paused;
uint16_t power_fault;
uint32_t lastClockSync;
bool is_screen_active;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { LocalStorageService } from 'src/app/local-storage.service';
type PoolLabel = 'Primary' | 'Fallback';
type MessageType =
| 'SYSTEM_INFO_ERROR'
| 'MINING_PAUSED'
| 'DEVICE_OVERHEAT'
| 'POWER_FAULT'
| 'FREQUENCY_LOW'
Expand Down Expand Up @@ -653,6 +654,7 @@ export class HomeComponent implements OnInit, OnDestroy {
};

updateMessage(!!systemInfoError.duration, 'SYSTEM_INFO_ERROR', 'error', `Unable to reach the device for ${DateAgoPipe.transform(systemInfoError.duration, { strict: true })}`);
updateMessage(!!(info as any).miningPaused, 'MINING_PAUSED', 'warn', 'Mining is paused');
updateMessage(info.overheat_mode === 1, 'DEVICE_OVERHEAT', 'error', 'Device has overheated - See settings');
updateMessage(!!info.power_fault, 'POWER_FAULT', 'error', `${info.power_fault} Check your Power Supply.`);
updateMessage(!info.frequency || info.frequency < 400, 'FREQUENCY_LOW', 'warn', 'Device frequency is set low - See settings');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ <h2>Swarm</h2>
class="inline-block"
styleClass="button-icon-small"
(click)="edit(axe)" />
<p-button
[icon]="'pi text-sm ' + (axe.miningPaused ? 'pi-play-circle' : 'pi-pause-circle')"
pp-button
[pTooltip]="axe.miningPaused ? 'Resume Mining' : 'Pause Mining'"
tooltipPosition="top"
severity="secondary"
class="inline-block"
styleClass="button-icon-small"
(click)="postAction(axe, axe.miningPaused ? 'resume' : 'pause')" />
<p-button
icon="pi pi-refresh text-sm"
pp-button
Expand Down Expand Up @@ -295,6 +304,15 @@ <h2>Swarm</h2>
class="inline-block"
styleClass="button-icon"
(click)="edit(axe)" />
<p-button
[icon]="'pi ' + (axe.miningPaused ? 'pi-play-circle' : 'pi-pause-circle')"
pp-button
[pTooltip]="axe.miningPaused ? 'Resume Mining' : 'Pause Mining'"
tooltipPosition="top"
severity="secondary"
class="inline-block"
styleClass="button-icon"
(click)="postAction(axe, axe.miningPaused ? 'resume' : 'pause')" />
<p-button
icon="pi pi-refresh"
pp-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ export class SwarmComponent implements OnInit, OnDestroy {

getDeviceNotification(axe: any): { color: string; msg: string } | undefined {
switch (true) {
case !!axe.miningPaused:
return { color: 'yellow', msg: 'Paused' };
case axe.overheat_mode === 1:
return { color: 'red', msg: 'Overheated' };
case !!axe.power_fault:
Expand Down
11 changes: 10 additions & 1 deletion main/http_server/axe-os/src/app/layout/app.topbar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
<wifi-icon [rssi]="info.wifiRSSI" />
</a>
</li>
<li>
<a class="block py-2 text-white cursor-pointer"
tooltipPosition="bottom"
[pTooltip]="isMiningPaused ? 'Resume Mining' : 'Pause Mining'"
(click)="toggleMiningPaused()"
>
<i class="pi text-xl block" [ngClass]="isMiningPaused ? 'pi-play-circle' : 'pi-pause-circle'"></i>
</a>
</li>
<li>
<a class="block py-2 text-white cursor-pointer"
tooltipPosition="bottom"
Expand All @@ -53,4 +62,4 @@
(click)="layoutService.onMenuToggle()">
<i class="pi pi-bars"></i>
</button>
</div>
</div>
21 changes: 21 additions & 0 deletions main/http_server/axe-os/src/app/layout/app.topbar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class AppTopBarComponent implements OnInit, OnDestroy {

public info$!: Observable<ISystemInfo>;
public sensitiveDataHidden: boolean = false;
public isMiningPaused: boolean = false;
public items!: MenuItem[];

@Input() isAPMode: boolean = false;
Expand All @@ -37,6 +38,12 @@ export class AppTopBarComponent implements OnInit, OnDestroy {
.subscribe((hidden: boolean) => {
this.sensitiveDataHidden = hidden;
});

this.info$.pipe(takeUntil(this.destroy$)).subscribe((info: ISystemInfo) => {
if ((info as any).miningPaused !== undefined) {
this.isMiningPaused = (info as any).miningPaused;
}
});
}

ngOnDestroy() {
Expand All @@ -48,6 +55,20 @@ export class AppTopBarComponent implements OnInit, OnDestroy {
this.sensitiveData.toggle();
}

public toggleMiningPaused() {
const action = this.isMiningPaused
? this.systemService.resumeMining()
: this.systemService.pauseMining();
const newPausedState = !this.isMiningPaused;
action.subscribe({
next: (response) => {
this.isMiningPaused = newPausedState;
this.toastr.success(response.message);
},
error: () => this.toastr.error('Failed to change mining state')
});
}

public restart() {
this.systemService.restart().subscribe({
next: () => this.toastr.success('Device restarted'),
Expand Down
28 changes: 27 additions & 1 deletion main/http_server/axe-os/src/app/services/system.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
SystemASIC as ISystemASIC,
SystemASICASICModelEnum,
SystemService as GeneratedSystemService,
Settings
Settings,
GenericResponse
} from 'src/app/generated';

import { environment } from '../../environments/environment';
Expand Down Expand Up @@ -140,6 +141,7 @@ export class SystemApiService {
coinbaseOutputs: [{value: 50, address: "payoutaddress"}],
coinbaseValueTotalSatoshis: 50,
coinbaseValueUserSatoshis: 50,
miningPaused: false,
}
).pipe(delay(1000));
}
Expand Down Expand Up @@ -238,6 +240,30 @@ export class SystemApiService {
return of('Block found notification dismissed (mock)');
}

public pauseMining(uri: string = '') {
if (environment.production && this.generatedSystemService && !uri) {
return this.generatedSystemService.pauseMining();
}

if (environment.production && uri) {
return this.httpClient.post<GenericResponse>(`${uri}/api/system/pause`, {});
}

return of<GenericResponse>({ message: 'Mining paused' });
}

public resumeMining(uri: string = '') {
if (environment.production && this.generatedSystemService && !uri) {
return this.generatedSystemService.resumeMining();
}

if (environment.production && uri) {
return this.httpClient.post<GenericResponse>(`${uri}/api/system/resume`, {});
}

return of<GenericResponse>({ message: 'Mining resumed' });
}

public identify(uri: string = '') {
if (environment.production && this.generatedSystemService && !uri) {
return this.generatedSystemService.identifySystem();
Expand Down
71 changes: 70 additions & 1 deletion main/http_server/http_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,58 @@ static esp_err_t POST_dismiss_block_found(httpd_req_t * req)
return res;
}

static esp_err_t POST_mining_pause(httpd_req_t * req)
{
if (is_network_allowed(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}

if (set_cors_headers(req) != ESP_OK) {
httpd_resp_send_500(req);
return ESP_OK;
}

GLOBAL_STATE->SYSTEM_MODULE.mining_paused = true;
ESP_LOGI(TAG, "Mining paused by API request");

httpd_resp_set_type(req, "application/json");
cJSON * resp = cJSON_CreateObject();
if (resp == NULL) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal error");
return ESP_OK;
}
cJSON_AddStringToObject(resp, "message", "Mining paused");
esp_err_t res = HTTP_send_json(req, resp, &api_common_prebuffer_len);
cJSON_Delete(resp);
return res;
}

static esp_err_t POST_mining_resume(httpd_req_t * req)
{
if (is_network_allowed(req) != ESP_OK) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}

if (set_cors_headers(req) != ESP_OK) {
httpd_resp_send_500(req);
return ESP_OK;
}

GLOBAL_STATE->SYSTEM_MODULE.mining_paused = false;
ESP_LOGI(TAG, "Mining resumed by API request");

httpd_resp_set_type(req, "application/json");
cJSON * resp = cJSON_CreateObject();
if (resp == NULL) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Internal error");
return ESP_OK;
}
cJSON_AddStringToObject(resp, "message", "Mining resumed");
esp_err_t res = HTTP_send_json(req, resp, &api_common_prebuffer_len);
cJSON_Delete(resp);
return res;
}

static const char* esp_reset_reason_to_string(esp_reset_reason_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN: return "Reset reason can not be determined";
Expand Down Expand Up @@ -933,6 +985,7 @@ static esp_err_t GET_system_info(httpd_req_t * req)
cJSON_AddStringToObject(root, "runningPartition", esp_ota_get_running_partition()->label);

cJSON_AddNumberToObject(root, "overheat_mode", nvs_config_get_bool(NVS_CONFIG_OVERHEAT_MODE));
cJSON_AddBoolToObject(root, "miningPaused", GLOBAL_STATE->SYSTEM_MODULE.mining_paused);
cJSON_AddNumberToObject(root, "overclockEnabled", nvs_config_get_bool(NVS_CONFIG_OVERCLOCK_ENABLED));
cJSON_AddStringToObject(root, "display", display);
cJSON_AddNumberToObject(root, "rotation", nvs_config_get_u16(NVS_CONFIG_ROTATION));
Expand Down Expand Up @@ -1385,8 +1438,24 @@ esp_err_t start_rest_server(void * pvParameters)
};
httpd_register_uri_handler(server, &system_restart_uri);

httpd_uri_t system_mining_pause_uri = {
.uri = "/api/system/pause",
.method = HTTP_POST,
.handler = POST_mining_pause,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_mining_pause_uri);
Comment thread
b-rowan marked this conversation as resolved.

httpd_uri_t system_mining_resume_uri = {
.uri = "/api/system/resume",
.method = HTTP_POST,
.handler = POST_mining_resume,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_mining_resume_uri);

httpd_uri_t system_dismiss_block_found_uri = {
.uri = "/api/system/blockFound/dismiss",
.uri = "/api/system/blockFound/dismiss",
.method = HTTP_POST,
.handler = POST_dismiss_block_found,
.user_ctx = NULL
Expand Down
42 changes: 42 additions & 0 deletions main/http_server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ components:
- coinbaseValueTotalSatoshis
- coinbaseValueUserSatoshis
- hashrateMonitor
- miningPaused
properties:
ASICModel:
type: string
Expand Down Expand Up @@ -487,6 +488,9 @@ components:
description: Hashrate register value per ASIC
items:
$ref: '#/components/schemas/HashrateMonitorAsic'
miningPaused:
type: boolean
description: Whether mining is currently paused

SystemASIC:
type: object
Expand Down Expand Up @@ -819,6 +823,44 @@ paths:
'500':
description: Internal server error

/api/system/pause:
post:
summary: Pause mining
description: Pauses mining activity
operationId: pauseMining
tags:
- system
responses:
'200':
description: Mining paused
content:
Comment thread
b-rowan marked this conversation as resolved.
application/json:
schema:
$ref: '#/components/schemas/GenericResponse'
'401':
description: Unauthorized - Client not in allowed network range
'500':
description: Internal server error

/api/system/resume:
post:
summary: Resume mining
description: Resumes mining activity
operationId: resumeMining
tags:
- system
responses:
'200':
description: Mining resumed
content:
application/json:
schema:
$ref: '#/components/schemas/GenericResponse'
'401':
description: Unauthorized - Client not in allowed network range
'500':
description: Internal server error

/api/system/restart:
post:
summary: Restart the system
Expand Down
8 changes: 5 additions & 3 deletions main/power/vcore.c
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ esp_err_t VCORE_set_voltage(GlobalState * GLOBAL_STATE, float core_voltage)
{
ESP_LOGI(TAG, "Set ASIC voltage = %.3fV", core_voltage);

// Enable/disable the ASIC power enable GPIO before touching the regulator
if (GLOBAL_STATE->DEVICE_CONFIG.asic_enable) {
gpio_set_level(GPIO_ASIC_ENABLE, core_voltage == 0.0f ? 1 : 0);
}

if (GLOBAL_STATE->DEVICE_CONFIG.DS4432U) {
if (core_voltage != 0.0f) {
ESP_RETURN_ON_ERROR(DS4432U_set_voltage(core_voltage), TAG, "DS4432U set voltage failed!");
Expand All @@ -129,9 +134,6 @@ esp_err_t VCORE_set_voltage(GlobalState * GLOBAL_STATE, float core_voltage)
uint16_t voltage_domains = GLOBAL_STATE->DEVICE_CONFIG.family.voltage_domains;
ESP_RETURN_ON_ERROR(TPS546_set_vout(core_voltage * voltage_domains), TAG, "TPS546 set voltage failed!");
}
if (core_voltage == 0.0f && GLOBAL_STATE->DEVICE_CONFIG.asic_enable) {
gpio_set_level(GPIO_ASIC_ENABLE, 1);
}

return ESP_OK;
}
Expand Down
2 changes: 1 addition & 1 deletion main/screen.c
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ static void screen_update_cb(lv_timer_t * timer)
current_shares_rejected = shares_rejected;
current_work_received = work_received;
} else {
lv_label_set_text(notification_label, "");
lv_label_set_text(notification_label, module->mining_paused ? "▐▐" : "");
}

if (module->show_new_block) {
Expand Down
2 changes: 2 additions & 0 deletions main/system.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ void SYSTEM_init_system(GlobalState * GLOBAL_STATE)
module->overheat_mode = nvs_config_get_bool(NVS_CONFIG_OVERHEAT_MODE);
ESP_LOGI(TAG, "Initial overheat_mode value: %d", module->overheat_mode);

module->mining_paused = false;

//Initialize power_fault fault mode
module->power_fault = 0;

Expand Down
4 changes: 3 additions & 1 deletion main/tasks/create_jobs_task.c
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ static void generate_work(GlobalState *GLOBAL_STATE, mining_notify *notification

// Check if ASIC is initialized before trying to send work
if (!GLOBAL_STATE->ASIC_initalized) {
ESP_LOGW(TAG, "ASIC not initialized, skipping job send");
if (!GLOBAL_STATE->SYSTEM_MODULE.mining_paused) {
ESP_LOGW(TAG, "ASIC not initialized, skipping job send");
}
// Clean up the job since we're not sending it
// Note: This job was never stored in active_jobs, so it's safe to free
free(next_job->jobid);
Expand Down
Loading
Loading