realtime logs

This commit is contained in:
Ben 2023-08-27 11:53:47 -04:00
parent 0ceb4532a4
commit bac6da555a
16 changed files with 173 additions and 13 deletions

View File

@ -11,7 +11,11 @@
"esp_netif.h": "c",
"esp_http_server.h": "c",
"esp_chip_info.h": "c",
"xtensa_context.h": "c"
"xtensa_context.h": "c",
"sys.h": "c",
"array": "c",
"string": "c",
"string_view": "c"
},
"editor.formatOnSave": true
}

View File

@ -19,6 +19,7 @@
"ngx-toastr": "^17.0.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"xterm": "^5.2.1",
"zone.js": "~0.13.0"
},
"devDependencies": {
@ -13163,6 +13164,11 @@
"node": ">=0.4"
}
},
"node_modules/xterm": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.2.1.tgz",
"integrity": "sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA=="
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -22,6 +22,7 @@
"ngx-toastr": "^17.0.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"xterm": "^5.2.1",
"zone.js": "~0.13.0"
},
"devDependencies": {

View File

@ -12,6 +12,7 @@ import { EditComponent } from './components/edit/edit.component';
import { HeaderComponent } from './components/header/header.component';
import { HomeComponent } from './components/home/home.component';
import { LoadingComponent } from './components/loading/loading.component';
import { ANSIPipe } from './pipes/ansi.pipe';
const components = [
AppComponent,
@ -23,7 +24,8 @@ const components = [
...components,
EditComponent,
HomeComponent,
LoadingComponent
LoadingComponent,
ANSIPipe
],
imports: [
BrowserModule,

View File

@ -89,15 +89,19 @@ export class EditComponent {
}
this.systemService.performWWWOTAUpdate(file)
.pipe(this.loadingService.lockUIUntilComplete())
.subscribe({
.pipe(
this.loadingService.lockUIUntilComplete(),
).subscribe({
next: (event) => {
if (event.type === HttpEventType.UploadProgress) {
this.websiteUpdateProgress = Math.round((event.loaded / (event.total as number)) * 100);
} else if (event.type === HttpEventType.Response) {
if (event.ok) {
this.toastrService.success('Website updated', 'Success!');
window.location.reload();
setTimeout(() => {
this.toastrService.success('Website updated', 'Success!');
window.location.reload();
}, 1000);
} else {
this.toastrService.error(event.statusText, 'Error');
}

View File

@ -6,6 +6,7 @@
top: 0;
right: 0;
left: 0;
z-index: 9;
}
.header-text {

View File

@ -113,4 +113,11 @@
</div>
<div class="card mt-2">
<h2>Realtime Logs</h2>
<div id="logs" #scrollContainer>
<div *ngFor="let log of logs">₿ {{log | ANSI}}</div>
</div>
</div>
</ng-container>

View File

@ -36,4 +36,18 @@ table>tr>td {
.button-row {
display: flex;
justify-content: space-between;
}
#logs {
height: 500px;
font-family: monospace;
border: 1px solid #304562;
overflow-y: scroll;
overflow-x: hidden;
>div {
max-width: 100%;
line-break: anywhere;
}
}

View File

@ -1,27 +1,47 @@
import { Component } from '@angular/core';
import { AfterViewChecked, Component, ElementRef, ViewChild } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { Observable } from 'rxjs';
import { LoadingService } from 'src/app/services/loading.service';
import { SystemService } from 'src/app/services/system.service';
import { WebsocketService } from 'src/app/services/web-socket.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
export class HomeComponent implements AfterViewChecked {
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;
public info$: Observable<any>;
public logs: string[] = [];
constructor(
private systemService: SystemService,
private toastr: ToastrService,
private loadingService: LoadingService
private loadingService: LoadingService,
private websocketService: WebsocketService
) {
this.info$ = this.systemService.getInfo().pipe(
this.loadingService.lockUIUntilComplete()
)
this.websocketService.ws$.subscribe({
next: (val) => {
this.logs.push(val);
if (this.logs.length > 100) {
this.logs.shift();
}
}
})
}
ngAfterViewChecked(): void {
if (this.scrollContainer?.nativeElement != null) {
this.scrollContainer.nativeElement.scrollTo({ left: 0, top: this.scrollContainer.nativeElement.scrollHeight, behavior: 'smooth' });
}
}
public restart() {
@ -31,3 +51,4 @@ export class HomeComponent {
this.toastr.success('Success!', 'Bitaxe restarted');
}
}

View File

@ -1,12 +1,11 @@
#loading {
background-color: rgba(0, 0, 0, 0.5);
pointer-events: none;
top: 0;
bottom: 0;
left: 0;
right: 0;
position: fixed;
z-index: 999;
z-index: 99999999999999999999;
}
#loading-text {

View File

@ -0,0 +1,8 @@
import { ANSIPipe } from './ansi.pipe';
describe('ANSIPipe', () => {
it('create an instance', () => {
const pipe = new ANSIPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'ANSI',
pure: true
})
export class ANSIPipe implements PipeTransform {
transform(value: string): string {
return value.slice(9, value.length - 5);
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { WebSocketService } from './web-socket.service';
describe('WebSocketService', () => {
let service: WebSocketService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(WebSocketService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
@Injectable({
providedIn: 'root'
})
export class WebsocketService {
public ws$: WebSocketSubject<string>;
constructor() {
this.ws$ = webSocket({
url: `ws://${window.location.host}/api/ws`,
deserializer: (e: MessageEvent) => { return e.data }
});
}
}

View File

@ -32,6 +32,9 @@
static const char *TAG = "http_server";
static GlobalState *GLOBAL_STATE;
static httpd_handle_t server = NULL;
static int fd = -1;
#define REST_CHECK(a, str, goto_tag, ...) \
do \
@ -377,6 +380,40 @@ esp_err_t POST_OTA_update(httpd_req_t *req)
return ESP_OK;
}
void log_to_websocket(const char *format, va_list args)
{
char log_buffer[1000];
vsnprintf(log_buffer, sizeof(log_buffer), format, args);
httpd_ws_frame_t ws_pkt;
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
ws_pkt.payload = (uint8_t *)log_buffer;
ws_pkt.len = strlen(log_buffer);
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
vprintf(format, args);
if (httpd_ws_send_frame_async(server, fd, &ws_pkt) != ESP_OK)
{
esp_log_set_vprintf(vprintf);
}
}
/*
* This handler echos back the received ws data
* and triggers an async send if certain message received
*/
esp_err_t echo_handler(httpd_req_t *req)
{
if (req->method == HTTP_GET)
{
ESP_LOGI(TAG, "Handshake done, the new connection was opened");
fd = httpd_req_to_sockfd(req);
esp_log_set_vprintf(log_to_websocket);
return ESP_OK;
}
return ESP_OK;
}
esp_err_t start_rest_server(void *pvParameters)
{
GLOBAL_STATE = (GlobalState *)pvParameters;
@ -389,7 +426,6 @@ esp_err_t start_rest_server(void *pvParameters)
REST_CHECK(rest_context, "No memory for rest context", err);
strlcpy(rest_context->base_path, base_path, sizeof(rest_context->base_path));
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_match_fn = httpd_uri_match_wildcard;
@ -432,6 +468,14 @@ esp_err_t start_rest_server(void *pvParameters)
.user_ctx = NULL};
httpd_register_uri_handler(server, &update_post_ota_www);
httpd_uri_t ws = {
.uri = "/api/ws",
.method = HTTP_GET,
.handler = echo_handler,
.user_ctx = NULL,
.is_websocket = true};
httpd_register_uri_handler(server, &ws);
/* URI handler for getting web server files */
httpd_uri_t common_get_uri = {
.uri = "/*",

View File

@ -5,4 +5,5 @@ CONFIG_PARTITION_TABLE_OFFSET=0x8000
CONFIG_PARTITION_TABLE_MD5=y
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
CONFIG_ESP_MAXIMUM_RETRY=3
CONFIG_ESP_MAXIMUM_RETRY=3
CONFIG_HTTPD_WS_SUPPORT=y