diff --git a/.github/workflows/docker-build-action.yaml b/.github/workflows/docker-build-action.yaml
index 7a4c9d2..75e736d 100644
--- a/.github/workflows/docker-build-action.yaml
+++ b/.github/workflows/docker-build-action.yaml
@@ -24,4 +24,4 @@ jobs:
with:
context: .
push: true
- tags: thingpulse/esp-iot-flasher:latest
\ No newline at end of file
+ tags: thingpulse/esp-app-market:latest
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index b4bb5ff..1faa793 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,5 +6,5 @@ RUN npm install
RUN npm run build --prod
#stage 2
FROM nginx:alpine
-COPY --from=node /app/dist/esp-iot-flasher /usr/share/nginx/html
+COPY --from=node /app/dist/esp-app-market /usr/share/nginx/html
COPY /nginx.conf /etc/nginx/conf.d/default.conf
\ No newline at end of file
diff --git a/README.md b/README.md
index 1178957..e742de2 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,7 @@
-# ESP-IoT-Flasher
+# ESP-Firmware-Store
-The ESP-IoT-Flasher is web-based tool which is made for simplicity of use.
-A device tester just needs to install the UART driver. After opening the application
-in his browser he selects the device and clicks `Flash & Test`. The web application
-then flashes a firmware to the device and the device responds with tests results from tests
-running on the device.
+The ESP-Firmware-Store is web-based tool which is made for simplicity of use.
-[![Running the test](https://img.youtube.com/vi/a3fYCeyGAyI/maxresdefault.jpg)](https://www.youtube.com/shorts/a3fYCeyGAyI)
## Development server
@@ -24,11 +19,11 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
### Building docker for local machine architecture
-`docker build -t thingpulse/esp-iot-flasher:1.0.2 . `
+`docker build -t thingpulse/esp-app-market:1.0.2 . `
### building docker on ARM for x86
-`docker buildx build --platform linux/amd64 -t thingpulse/esp-iot-flasher:1.0.2 .`
+`docker buildx build --platform linux/amd64 -t thingpulse/esp-app-market:1.0.2 .`
### Running with docker-compose
@@ -40,9 +35,7 @@ will start the service at http://localhost:8081
## Changing device configuration
Default configurations loads the browser from the server. The angular application looks for a configuration
-file at `/assets/defaultDeviceConfiguration.json`:
-
-https://github.com/ThingPulse/esp-iot-flasher/blob/1c669c1fe53238a759b6027cf89888fffa4055e9/src/assets/defaultDeviceConfiguration.json#L1-L24
+file at `/assets/defaultDeviceConfiguration.json`
### Explanation
@@ -66,7 +59,7 @@ Adapt and uncomment the following lines to use your own configuration files in `
## Creating firmware to run the test
The following repository shows how to build a firmware which can be used together with the
-esp-iot-flasher: https://github.com/ThingPulse/esp32-epulse-feather-testbed
+esp-app-market: https://github.com/ThingPulse/esp32-epulse-feather-testbed
## FAQ
diff --git a/angular.json b/angular.json
index f1c138b..c68a004 100644
--- a/angular.json
+++ b/angular.json
@@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
- "esp-iot-flasher": {
+ "esp-app-market": {
"projectType": "application",
"schematics": {},
"root": "",
@@ -13,7 +13,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
- "outputPath": "dist/esp-iot-flasher",
+ "outputPath": "dist/esp-app-market",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
@@ -61,10 +61,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
- "browserTarget": "esp-iot-flasher:build:production"
+ "browserTarget": "esp-app-market:build:production"
},
"development": {
- "browserTarget": "esp-iot-flasher:build:development"
+ "browserTarget": "esp-app-market:build:development"
}
},
"defaultConfiguration": "development"
@@ -72,7 +72,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
- "browserTarget": "esp-iot-flasher:build"
+ "browserTarget": "esp-app-market:build"
}
},
"test": {
diff --git a/docker-compose.yaml b/docker-compose.yaml
index d585433..f13e296 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,8 +1,8 @@
version: '2'
services:
message-server:
- container_name: esp-iot-flasher
- image: thingpulse/esp-iot-flasher:1.0.8
+ container_name: esp-app-market
+ image: thingpulse/esp-app-market:1.0.8
restart: always
ports:
- 8081:80
diff --git a/package-lock.json b/package-lock.json
index e63a09b..07a1566 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "esp-iot-flasher",
+ "name": "esp-app-market",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "esp-iot-flasher",
+ "name": "esp-app-market",
"version": "0.0.0",
"dependencies": {
"@angular/animations": "^15.0.0",
diff --git a/package.json b/package.json
index 638d583..417b44a 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "esp-iot-flasher",
+ "name": "esp-app-market",
"version": "0.0.0",
"scripts": {
"ng": "ng",
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 1c06818..4057d93 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -2,8 +2,8 @@
- Firmware Store
- share Github
+ ESP App Market
+ share Github
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index 05c0d7d..10f7cd1 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -16,16 +16,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
});
- it(`should have as title 'esp-iot-flasher'`, () => {
+ it(`should have as title 'esp-app-market'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
- expect(app.title).toEqual('esp-iot-flasher');
+ expect(app.title).toEqual('esp-app-market');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('.content span')?.textContent).toContain('esp-iot-flasher app is running!');
+ expect(compiled.querySelector('.content span')?.textContent).toContain('esp-app-market app is running!');
});
});
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 340f395..58184e7 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -6,14 +6,26 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { MaterialModule } from './material/material.module';
import { RouterModule, Routes } from '@angular/router';
+import { DevicesComponent } from './devices/devices.component';
+import { AppsComponent } from './apps/apps.component';
+import { CacheInterceptor } from './services/cache.interceptor';
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { FlasherComponent } from './flasher/flasher.component';
const routes: Routes = [
- { path: '**', redirectTo: '/', pathMatch: 'full' }
+ { path: '', component: DevicesComponent },
+ { path: 'device/:deviceId', component: AppsComponent },
+ { path: 'device/:deviceId/app/:appId', component: FlasherComponent },
+ { path: '**', redirectTo: '/', pathMatch: 'full' },
+
];
@NgModule({
declarations: [
- AppComponent
+ AppComponent,
+ DevicesComponent,
+ AppsComponent,
+ FlasherComponent
],
imports: [
BrowserModule,
@@ -23,7 +35,9 @@ const routes: Routes = [
FormsModule,
RouterModule.forRoot(routes)
],
- providers: [],
+ providers: [
+ { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true },
+ ],
bootstrap: [AppComponent]
})
export class AppModule { }
diff --git a/src/app/apps/apps.component.css b/src/app/apps/apps.component.css
new file mode 100644
index 0000000..8bed319
--- /dev/null
+++ b/src/app/apps/apps.component.css
@@ -0,0 +1,3 @@
+.mdc-card {
+ margin-bottom: 10px;
+}
\ No newline at end of file
diff --git a/src/app/apps/apps.component.html b/src/app/apps/apps.component.html
new file mode 100644
index 0000000..f373ebe
--- /dev/null
+++ b/src/app/apps/apps.component.html
@@ -0,0 +1,25 @@
+
{{device?.name}}
+
+
+
+ {{device?.name}}
+ {{device?.manufacturer}}
+
+
+
+ {{device?.description}}
+
+
+
+
+
+
+ {{app.name}}
+ {{app?.version}}
+
+ {{app?.description}}
+
+
+
+
+
diff --git a/src/app/apps/apps.component.spec.ts b/src/app/apps/apps.component.spec.ts
new file mode 100644
index 0000000..950d321
--- /dev/null
+++ b/src/app/apps/apps.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AppsComponent } from './apps.component';
+
+describe('AppsComponent', () => {
+ let component: AppsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ AppsComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AppsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/apps/apps.component.ts b/src/app/apps/apps.component.ts
new file mode 100644
index 0000000..2318d0a
--- /dev/null
+++ b/src/app/apps/apps.component.ts
@@ -0,0 +1,34 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { DevicesService } from '../services/devices.service';
+import { Device } from '../models/device';
+import { AppsService } from '../services/apps.service';
+import { App } from '../models/app';
+
+@Component({
+ selector: 'app-apps',
+ templateUrl: './apps.component.html',
+ styleUrls: ['./apps.component.css']
+})
+export class AppsComponent implements OnInit{
+
+ deviceId: string;
+ device: Device | undefined;
+ apps: App[];
+
+ constructor(private route: ActivatedRoute,
+ public deviceService: DevicesService,
+ public appService: AppsService ) { }
+
+
+ ngOnInit(): void {
+ this.deviceId = this.route.snapshot.paramMap.get("deviceId")!;
+ console.log(this.deviceId);
+ this.deviceService.findById(this.deviceId).subscribe((device) => { this.device = device; })
+ console.log(this.device);
+ this.appService.findByDeviceId(this.deviceId).subscribe((apps) => {
+ this.apps = apps;
+ });
+ }
+
+}
diff --git a/src/app/devices/devices.component.css b/src/app/devices/devices.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/devices/devices.component.html b/src/app/devices/devices.component.html
new file mode 100644
index 0000000..f5e9e36
--- /dev/null
+++ b/src/app/devices/devices.component.html
@@ -0,0 +1,13 @@
+
+
+
+ {{device.name}}
+ {{device.manufacturer}}
+
+
+
+ {{device.description}}
+
+
+
+
diff --git a/src/app/devices/devices.component.spec.ts b/src/app/devices/devices.component.spec.ts
new file mode 100644
index 0000000..c31f7dd
--- /dev/null
+++ b/src/app/devices/devices.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DevicesComponent } from './devices.component';
+
+describe('DevicesComponent', () => {
+ let component: DevicesComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ DevicesComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(DevicesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/devices/devices.component.ts b/src/app/devices/devices.component.ts
new file mode 100644
index 0000000..888b5ca
--- /dev/null
+++ b/src/app/devices/devices.component.ts
@@ -0,0 +1,22 @@
+import { Component, OnInit } from '@angular/core';
+import { DevicesService } from '../services/devices.service';
+import { Device } from '../models/device';
+
+@Component({
+ selector: 'app-devices',
+ templateUrl: './devices.component.html',
+ styleUrls: ['./devices.component.css']
+})
+export class DevicesComponent implements OnInit {
+
+ devices: Device[] = [];
+
+ constructor(public deviceService: DevicesService) { }
+
+ ngOnInit(): void {
+ this.deviceService.getDevices().subscribe((devices) => {
+ this.devices = devices;
+ });
+ }
+
+}
diff --git a/src/app/flasher/flasher.component.css b/src/app/flasher/flasher.component.css
new file mode 100644
index 0000000..0e83513
--- /dev/null
+++ b/src/app/flasher/flasher.component.css
@@ -0,0 +1,19 @@
+.flex-container {
+ display: flex;
+ justify-content: space-around;
+ flex-wrap: wrap;
+ padding: 16px;
+ }
+
+.flex-row {
+ display: flex; /* or inline-flex */
+ padding-right: 10px;
+ gap: 20px;
+ align-items: baseline;
+ justify-content: space-between;
+ }
+
+ .mdc-card {
+ margin-bottom: 10px;
+}
+
diff --git a/src/app/flasher/flasher.component.html b/src/app/flasher/flasher.component.html
new file mode 100644
index 0000000..5a13bf6
--- /dev/null
+++ b/src/app/flasher/flasher.component.html
@@ -0,0 +1,57 @@
+
+
+
+ {{app?.name}}
+ {{app?.version}}
+
+
+ {{app?.description}}
+ {{app?.instructions}}
+
+
+ {{flasherConsole}}
+
+
+ {{ partition.name }} {{progresses[i].progress}}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ phonelink Connected
+
+
+ phonelink_off Disconnected
+
+
+
+
+
+
+
+
+
+
+
+ Serial Console Output
+
+
+
+ Messages: {{messageCount}}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/flasher/flasher.component.spec.ts b/src/app/flasher/flasher.component.spec.ts
new file mode 100644
index 0000000..076c7b7
--- /dev/null
+++ b/src/app/flasher/flasher.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FlasherComponent } from './flasher.component';
+
+describe('FlasherComponent', () => {
+ let component: FlasherComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ FlasherComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(FlasherComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/flasher/flasher.component.ts b/src/app/flasher/flasher.component.ts
new file mode 100644
index 0000000..af15388
--- /dev/null
+++ b/src/app/flasher/flasher.component.ts
@@ -0,0 +1,138 @@
+import { Component, OnInit } from '@angular/core';
+import { Device } from '../models/device';
+import { ActivatedRoute } from '@angular/router';
+import { AppsService } from '../services/apps.service';
+import { DevicesService } from '../services/devices.service';
+import { App } from '../models/app';
+import { EspPortService } from '../services/esp-port.service';
+import { PartitionProgress } from '../services/utils.service';
+import { Subscription } from 'rxjs';
+
+@Component({
+ selector: 'app-flasher',
+ templateUrl: './flasher.component.html',
+ styleUrls: ['./flasher.component.css']
+})
+export class FlasherComponent implements OnInit{
+
+ deviceId: string;
+ appId: string;
+ device: Device | undefined;
+ app: App | undefined;
+
+ private subscriptions: Subscription = new Subscription();
+
+ private connected = false;
+ private monitoring = false;
+
+ messageArea: string = "";
+ messageCount = 0;
+ flasherConsole: string;
+ progresses: PartitionProgress[] = new Array();
+
+ constructor(private route: ActivatedRoute,
+ public deviceService: DevicesService,
+ public appService: AppsService,
+ public portService: EspPortService ) { }
+
+
+ ngOnInit(): void {
+ this.deviceId = this.route.snapshot.paramMap.get("deviceId")!;
+ this.appId = this.route.snapshot.paramMap.get("appId")!;
+ console.log(this.deviceId);
+ this.deviceService.findById(this.deviceId).subscribe((device) => { this.device = device; })
+ console.log(this.device);
+ this.appService.findById(this.appId).subscribe((app) => {
+ this.app = app;
+ });
+ const portStateStreamSubscription = this.portService.portStateStream.subscribe(isConnected => {
+ console.log("isConnected: ", isConnected);
+ this.connected = isConnected;
+ });
+ this.subscriptions.add(portStateStreamSubscription);
+ const monitorStateSubscription = this.portService.monitorStateStream.subscribe(isMonitoring => {
+ console.log("isMonitoring: ", isMonitoring);
+ this.monitoring = isMonitoring;
+ });
+ this.subscriptions.add(monitorStateSubscription);
+
+ const monitorStreamSubscription = this.portService.monitorMessageStream.subscribe(message => {
+
+ this.messageArea = this.messageArea + '\n' + message;
+ this.messageCount++;
+
+
+ });
+ this.subscriptions.add(monitorStreamSubscription);
+
+ const flashProgressStreamSubscription = this.portService.flashProgressStream.subscribe(progress => {
+ this.progresses[progress.index] = progress;
+ });
+ this.subscriptions.add(flashProgressStreamSubscription);
+
+ const testStateStreamSubscription = this.portService.testStateStream.subscribe(state => {
+ console.log("Test State: ", state);
+ });
+ this.subscriptions.add(testStateStreamSubscription);
+ }
+
+ resetState() {
+ this.messageArea = ""
+ this.messageCount = 0;
+ this.flasherConsole = "Ready";
+ this.progresses = new Array(this.app?.partitions.length);
+ }
+
+ connect() {
+ try {
+ this.portService.connect();
+ } catch (e) {
+ this.flasherConsole = "Could not open port. Please close all open monitoring sessions or refresh this browser.";
+ }
+ }
+
+ close() {
+ this.portService.close();
+ }
+
+ isConnected() {
+ return this.connected;
+ }
+
+ isMonitoring() {
+ return this.monitoring;
+ }
+
+ startMonitor() {
+ this.portService.startMonitor();
+ }
+
+ stopMonitor() {
+ this.portService.stopMonitor();
+ }
+
+ reset() {
+ this.portService.resetDevice();
+ }
+
+ async flash() {
+ console.log("Flashing");
+ this.resetState();
+ if (this.app && this.app.partitions) {
+ try {
+ await this.portService.connect();
+ console.log("Connected");
+ } catch (e) {
+ console.log(e);
+ //this.flasherConsole = "Could not open port. Please close all open monitoring sessions or refresh this browser.";
+ return;
+ }
+ console.log("Flashing");
+ await this.portService.flash(this.app.partitions);
+ } else {
+ console.log("No app selected");
+ }
+
+ }
+
+}
diff --git a/src/app/models/app.ts b/src/app/models/app.ts
new file mode 100644
index 0000000..b80f1a2
--- /dev/null
+++ b/src/app/models/app.ts
@@ -0,0 +1,13 @@
+import { Partition } from "../services/utils.service";
+
+export interface App {
+ id: string;
+ name: string;
+ description: string;
+ version: string;
+ repository: string;
+ appIcon: string;
+ instructions: string;
+ supportedDevices: string[];
+ partitions: Partition[];
+}
\ No newline at end of file
diff --git a/src/app/models/device.ts b/src/app/models/device.ts
new file mode 100644
index 0000000..c9bcf46
--- /dev/null
+++ b/src/app/models/device.ts
@@ -0,0 +1,8 @@
+export interface Device {
+ id: string;
+ name: string;
+ manufacturer: string;
+ imageThumbnail: string;
+ productLink: string;
+ description: string;
+}
diff --git a/src/app/services/apps.service.spec.ts b/src/app/services/apps.service.spec.ts
new file mode 100644
index 0000000..ae06cee
--- /dev/null
+++ b/src/app/services/apps.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AppsService } from './apps.service';
+
+describe('AppsService', () => {
+ let service: AppsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(AppsService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/apps.service.ts b/src/app/services/apps.service.ts
new file mode 100644
index 0000000..3570f9a
--- /dev/null
+++ b/src/app/services/apps.service.ts
@@ -0,0 +1,31 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { App } from '../models/app';
+import { Observable, map } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AppsService {
+
+ constructor(public httpClient: HttpClient) {
+
+ }
+
+ getApps(): Observable {
+ return this.httpClient.get('/assets/apps.json');
+ }
+
+ findByDeviceId(deviceId: string): Observable {
+ return this.getApps().pipe(
+ map(apps => apps.filter(app => app.supportedDevices.includes(deviceId)))
+ );
+ }
+
+
+ findById(id: string): Observable {
+ return this.getApps().pipe(
+ map(apps => apps.find(app => app.id === id))
+ );
+ }
+}
diff --git a/src/app/services/cache.interceptor.ts b/src/app/services/cache.interceptor.ts
new file mode 100644
index 0000000..5a46e5d
--- /dev/null
+++ b/src/app/services/cache.interceptor.ts
@@ -0,0 +1,31 @@
+import { Injectable } from '@angular/core';
+import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
+import { Observable, of } from 'rxjs';
+import { tap } from 'rxjs/operators';
+
+@Injectable()
+export class CacheInterceptor implements HttpInterceptor {
+ private cache = new Map>();
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ // Only cache GET requests
+ if (req.method !== 'GET') {
+ return next.handle(req);
+ }
+
+ // Check if the request is in the cache
+ const cachedResponse = this.cache.get(req.url);
+ if (cachedResponse) {
+ return of(cachedResponse.clone());
+ }
+
+ // If the request is not in the cache, send it to the server and cache the response
+ return next.handle(req).pipe(
+ tap(event => {
+ if (event instanceof HttpResponse) {
+ this.cache.set(req.url, event.clone());
+ }
+ })
+ );
+ }
+}
diff --git a/src/app/services/devices.service.spec.ts b/src/app/services/devices.service.spec.ts
new file mode 100644
index 0000000..69047c1
--- /dev/null
+++ b/src/app/services/devices.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { DevicesService } from './devices.service';
+
+describe('DevicesService', () => {
+ let service: DevicesService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(DevicesService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/devices.service.ts b/src/app/services/devices.service.ts
new file mode 100644
index 0000000..2c74981
--- /dev/null
+++ b/src/app/services/devices.service.ts
@@ -0,0 +1,26 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable, OnInit } from '@angular/core';
+import { Device } from '../models/device';
+import { Observable, map, of } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DevicesService {
+
+ constructor(public httpClient: HttpClient) {
+
+ }
+
+
+ getDevices(): Observable {
+ return this.httpClient.get('/assets/devices.json');
+ }
+
+
+ findById(id: string): Observable {
+ return this.getDevices().pipe(
+ map(devices => devices.find(device => device.id === id))
+ );
+ }
+}
diff --git a/src/app/services/esp-port.service.spec.ts b/src/app/services/esp-port.service.spec.ts
new file mode 100644
index 0000000..99627e5
--- /dev/null
+++ b/src/app/services/esp-port.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { EspPortService } from './esp-port.service';
+
+describe('EspPortService', () => {
+ let service: EspPortService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(EspPortService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/esp-port.service.ts b/src/app/services/esp-port.service.ts
new file mode 100644
index 0000000..3995cc5
--- /dev/null
+++ b/src/app/services/esp-port.service.ts
@@ -0,0 +1,301 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { ESPLoader, FlashOptions, IEspLoaderTerminal, LoaderOptions, Transport } from "esptool-js";
+import { firstValueFrom, Subject } from 'rxjs';
+import { LineBreakTransformer, Partition, PartitionProgress, sleep, TestState } from './utils.service';
+import { MD5, enc } from 'crypto-js';
+
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EspPortService {
+
+ private connected = false;
+ private monitorPort = true;
+ port!: SerialPort;
+
+ private controlCharacter: string = "\n";
+
+ private transport: Transport;
+ private esploader: ESPLoader;
+
+ // Publishes state changes of the selected serial port
+ private portStateSource = new Subject();
+ portStateStream = this.portStateSource.asObservable();
+
+ // Publishes the state of the console monitor
+ private monitorStateSource = new Subject();
+ monitorStateStream = this.monitorStateSource.asObservable();
+
+ // Publishes the console messages
+ private monitorMessageSource = new Subject();
+ monitorMessageStream = this.monitorMessageSource.asObservable();
+
+ // Publishes the progress of the flashing process for each partition
+ private flashProgressSource = new Subject();
+ flashProgressStream = this.flashProgressSource.asObservable();
+
+ // Publishes the progress/state of the test
+ private testStateSource = new Subject();
+ testStateStream = this.testStateSource.asObservable();
+
+ // Characters contained in the first messages after reboot in an ESP32
+ private resetMessageMatchers: string[] = ['rst:0x1', 'configsip', 'mode:DIO', 'entry 0x', 'READY_FOR_SELFTEST'];
+ private selfTestMatchers: string[] = ['READY_FOR_SELFTEST'];
+
+ private reader!: ReadableStreamDefaultReader;
+ private readableStreamClosed!: any;
+
+ private espLoaderTerminal = {
+ clean: () => {
+ this.monitorMessageSource.next("Clean");
+ },
+ writeLine: (data: any) => {
+ this.monitorMessageSource.next(data);
+ },
+ write: (data: any) => {
+ this.monitorMessageSource.next(data);
+ },
+ };
+
+
+ constructor(public httpClient: HttpClient) {
+ }
+
+ async connect() {
+
+ // If port is still open close it first
+ if (this.port && this.connected) {
+ console.log("Port still seems to be connected. Closing");
+ await this.close();
+ this.setState(false);
+ }
+
+ const port = await navigator.serial.requestPort();
+ this.transport = new Transport(port);
+ try {
+
+ const flashOptions = {
+ transport: this.transport,
+ baudrate: 115200,
+ terminal: this.espLoaderTerminal
+
+ } as LoaderOptions;
+ this.esploader = new ESPLoader(flashOptions);
+
+ const chip = await this.esploader.main_fn();
+ console.log(this.esploader.chip);
+ } catch (e) {
+ console.error(e);
+ }
+ await this.openPort(port);
+
+ }
+
+ async openPort(port: SerialPort) {
+ this.port = port;
+ console.log('oppening port:', port)
+ this.port.addEventListener('connect', (event) => {
+ this.setState(true);
+ });
+ this.port.addEventListener('disconnect', (event) => {
+ this.setState(false);
+ });
+ if (!this.port.readable) {
+ await this.port.open({ baudRate: 115200 });
+ }
+ const portInfo = port.getInfo();
+ console.log(portInfo);
+ this.setState(true);
+
+ }
+
+ checkForRestart(message: string) {
+ // Check the given console message for some trigger characters
+ // and publish a message if that is the case
+ for (let matcher of this.resetMessageMatchers) {
+ if (message.indexOf(matcher) > -1) {
+ this.testStateSource.next(TestState.Restarted);
+ break;
+ }
+ }
+
+ }
+
+ checkForTesting(message: string) {
+ // Check the given console message for some trigger characters
+ // and publish a message if that is the case
+ for (let matcher of this.selfTestMatchers) {
+ if (message.indexOf(matcher) > -1) {
+ this.testStateSource.next(TestState.Testing);
+ break;
+ }
+ }
+
+ }
+
+ async reconnect() {
+ try {
+ await this.port.close();
+ console.log("Port closed");
+ this.setState(false);
+ await this.openPort(this.port);
+ }
+ catch (e) {
+ console.log('Error clossing port', this.port, e)
+ }
+ }
+
+ setState(isConnected: boolean) {
+ this.connected = isConnected;
+ this.portStateSource.next(this.connected);
+ this.testStateSource.next(isConnected ? TestState.Connected : TestState.Initial);
+ }
+
+ async sendSelfTestCommand() {
+ console.log("Sending self test command");
+ const encoder = new TextEncoder();
+ const writer = this.port.writable?.getWriter();
+ if (writer) {
+ await writer.write(encoder.encode("SELFTEST\n"));
+ writer.releaseLock();
+ }
+ }
+
+
+ async resetDevice() {
+ /*
+ State table of the programming circuit
+ DTR RTS -> EN IO0
+ 1 1 1 1
+ 0 0 1 1
+ 1 0 0 1
+ 0 1 1 0
+ */
+ console.log("Resetting device");
+ this.testStateSource.next(TestState.Restarting);
+ await this.port.setSignals({ dataTerminalReady: false});
+ await this.port.setSignals({ requestToSend: true});
+ sleep(100);
+ await this.port.setSignals({ dataTerminalReady: true});
+ await this.port.setSignals({ requestToSend: false});
+ sleep(50);
+ await this.port.setSignals({ dataTerminalReady: false});
+ }
+
+ setMonitorState(isMonitoring: boolean) {
+ this.monitorPort = isMonitoring;
+ this.monitorStateSource.next(this.monitorPort);
+ }
+
+ startMonitor() {
+ this.setMonitorState(true);
+ this.readLoop();
+ }
+
+ async stopMonitor() {
+ this.setMonitorState(false);
+ await this.reader.cancel();
+ await this.readableStreamClosed.catch(() => { });
+ }
+
+ async readLoop() {
+ console.log("Is port readable: " + this.port.readable);
+ while (this.port.readable && this.monitorPort) {
+ const textDecoder = new TextDecoderStream();
+ this.readableStreamClosed = this.port.readable.pipeTo(textDecoder.writable);
+ this.reader = textDecoder.readable
+ .pipeThrough(new TransformStream(new LineBreakTransformer(this.controlCharacter)))
+ .getReader();
+
+ try {
+ while (true) {
+ const { value, done } = await this.reader.read();
+ if (done) {
+ console.log("Done reading");
+ this.reader.releaseLock();
+ break;
+ }
+ if (value && value !== "") {
+ console.log(value);
+ this.checkForRestart(value);
+ this.checkForTesting(value);
+ this.monitorMessageSource.next(value);
+ }
+ }
+ } catch (error) {
+ console.error("Read Loop error.", error);
+ }
+ console.log(".");
+ }
+ console.log("Leaving read loop...");
+ }
+
+ async flash(partitions: Partition[]) {
+ await this.loadData(partitions);
+ try {
+ console.log("connecting...");
+
+
+ try {
+ const fileArray = [];
+ //const progressBars = [];
+ for (let i = 0; i < partitions.length; i++) {
+ fileArray.push({ data: partitions[i].data, address: partitions[i].offset });
+ }
+ try {
+ const flashOptions: FlashOptions = {
+ fileArray: fileArray,
+ flashSize: "keep",
+ eraseAll: false,
+ compress: true,
+ reportProgress: (fileIndex, written, total) => {
+ this.flashProgressSource.next({index: fileIndex, progress: Math.round((written / total) * 100)});
+ },
+ calculateMD5Hash: (image) => MD5(enc.Latin1.parse(image)).toString(),
+ } as FlashOptions;
+ this.testStateSource.next(TestState.Flashing);
+ await this.esploader.write_flash(flashOptions);
+ } catch (e) {
+ console.error(e);
+
+ }
+ console.log("successfully written device partitions");
+ console.log("flashing succeeded");
+ this.testStateSource.next(TestState.Flashed);
+ await this.esploader.flash_finish(false);
+ } finally {
+ await this.esploader.hard_reset();
+ }
+ } finally {
+ console.log("Done flashing");
+ }
+ }
+
+ async loadData(partitions: Partition[]) {
+ this.testStateSource.next(TestState.LoadingFirmware);
+ await Promise.all(partitions.map(async (partition) => {
+ let buffer = await firstValueFrom(this.httpClient.get(partition.url, { responseType: 'arraybuffer' }));
+ console.log("Array Buffer Length: %d", buffer.byteLength);
+ var byteArray = new Uint8Array(buffer);
+ var decoder = new TextDecoder();
+ var value: number;
+ for (var i = 0; i < byteArray.length; i++) {
+ partition.data += String.fromCharCode((byteArray.at(i) || 0));
+ }
+
+ }));
+ }
+
+ async close() {
+ try {
+ await this.port.close();
+ console.log("Port closed");
+ this.setState(false);
+ } catch (e) {
+ console.log('Error clossing port', this.port, e)
+ }
+ }
+}
diff --git a/src/app/services/utils.service.spec.ts b/src/app/services/utils.service.spec.ts
new file mode 100644
index 0000000..d70aee3
--- /dev/null
+++ b/src/app/services/utils.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UtilsService } from './utils.service';
+
+describe('UtilsService', () => {
+ let service: UtilsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(UtilsService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/utils.service.ts b/src/app/services/utils.service.ts
new file mode 100644
index 0000000..c5f72bf
--- /dev/null
+++ b/src/app/services/utils.service.ts
@@ -0,0 +1,76 @@
+import { Injectable } from '@angular/core';
+
+
+export async function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export type Partition = {
+ name: string;
+ data: string;
+ offset: number;
+ url: string;
+};
+
+export type PartitionProgress = {
+ index: number;
+ progress: number;
+};
+
+
+export enum TestState {
+ Initial = "Initial",
+ Connected = "Connected",
+ LoadingFirmware = "Loading Firmware",
+ Flashing = "Flashing",
+ Flashed = "Flashed",
+ Restarting = "Restarting",
+ Restarted = "Restarted",
+ Testing = "Testing",
+ Tested = "Tested",
+}
+
+export class LineBreakTransformer {
+ container: any = "";
+ private controlCharacter: string;
+
+ constructor(controlCharacter: string) {
+ this.container = '';
+ this.controlCharacter = controlCharacter
+ }
+
+ transform(chunk: any, controller: any) {
+ this.container += chunk;
+ const lines = this.container.split(this.controlCharacter);
+ this.container = lines.pop();
+ lines.forEach((line: any) => controller.enqueue(line));
+ }
+
+ flush(controller: any) {
+ controller.enqueue(this.container);
+ }
+}
+
+export class JsonTransformer {
+
+ container: any = "";
+ transform(chunk: any, controller: any) {
+ try {
+ controller.enqueue(JSON.parse(chunk));
+ } catch(e) {
+ console.log("Not a valid JSON. Chunk: ", chunk, e);
+ }
+ }
+
+ flush(controller: any) {
+ controller.enqueue(this.container);
+ }
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UtilsService {
+
+ constructor() { }
+}
diff --git a/src/assets/apps.json b/src/assets/apps.json
new file mode 100644
index 0000000..ebbdf1a
--- /dev/null
+++ b/src/assets/apps.json
@@ -0,0 +1,49 @@
+[
+ {
+ "id": "tp-pendrive-s3-super-wifi-duck",
+ "name": "Super Wifi Duck",
+ "description": "BadUsb/ Keystroke Injection/ Wifi Duck",
+ "version": "1.0.0",
+ "repository": "https://github.com/ThingPulse/SuperWiFiDuck",
+ "appIcon": "/assets/apps/super-wifi-duck/super-wifi-duck.png",
+ "supportedDevices": [
+ "tp-pendrive-s3"
+ ],
+ "partitions": [{
+ "name": "Firmware",
+ "data": [],
+ "offset": 0,
+ "url": "./assets/apps/super-wifi-duck/app-firmware.bin"
+ }]
+ },
+ {
+ "id": "tp-pendrive-s3-circuit-python",
+ "name": "Circuit Python",
+ "description": "Circuit Python",
+ "instructions": "",
+ "version": "1.0.0",
+ "repository": "https://github.com/adafruit/circuitpython",
+ "appIcon": "/assets/apps/circuitpython/circuitpython-logo.png",
+ "supportedDevices": [
+ "tp-pendrive-s3"
+ ],
+ "partitions": [{
+ "name": "Firmware",
+ "data": [],
+ "offset": 0,
+ "url": "./assets/apps/circuitpython/circuit-python-bootloader.bin"
+ }]
+ },
+ {
+ "id": "other-device",
+ "name": "Weird Firmware",
+ "description": "Something",
+ "version": "1.0.0",
+ "repository": "https://github.com/ThingPulse/SuperWiFiDuck",
+ "appIcon": "/assets/apps/super-wifi-duck/super-wifi-duck.png",
+ "supportedDevices": [
+ "tp-default-device"
+ ],
+ "partitions": []
+ }
+]
\ No newline at end of file
diff --git a/src/assets/apps/circuitpython/circuit-python-bootloader.bin b/src/assets/apps/circuitpython/circuit-python-bootloader.bin
new file mode 100644
index 0000000..e628ec1
Binary files /dev/null and b/src/assets/apps/circuitpython/circuit-python-bootloader.bin differ
diff --git a/src/assets/apps/circuitpython/circuit-python.uf2 b/src/assets/apps/circuitpython/circuit-python.uf2
new file mode 100644
index 0000000..2736b68
Binary files /dev/null and b/src/assets/apps/circuitpython/circuit-python.uf2 differ
diff --git a/src/assets/apps/circuitpython/circuitpython-logo.png b/src/assets/apps/circuitpython/circuitpython-logo.png
new file mode 100644
index 0000000..760cd07
Binary files /dev/null and b/src/assets/apps/circuitpython/circuitpython-logo.png differ
diff --git a/src/assets/apps/super-wifi-duck/app-firmware.bin b/src/assets/apps/super-wifi-duck/app-firmware.bin
new file mode 100644
index 0000000..f1625e3
Binary files /dev/null and b/src/assets/apps/super-wifi-duck/app-firmware.bin differ
diff --git a/src/assets/apps/super-wifi-duck/super-wifi-duck.png b/src/assets/apps/super-wifi-duck/super-wifi-duck.png
new file mode 100644
index 0000000..1f847fd
Binary files /dev/null and b/src/assets/apps/super-wifi-duck/super-wifi-duck.png differ
diff --git a/src/assets/devices.json b/src/assets/devices.json
index 2174913..7c9bd74 100644
--- a/src/assets/devices.json
+++ b/src/assets/devices.json
@@ -1,9 +1,9 @@
[
{
- "id": 1,
+ "id": "tp-pendrive-s3",
"name": "Pendrive S3",
"manufacturer": "ThingPulse",
- "image-thumbnail": "assets/devices/pendrive-s3/pendrive-s3.jpg",
+ "imageThumbnail": "assets/devices/pendrive-s3/pendrive-s3-thumb.jpg",
"productLink": "https://thingpulse.com/product/pendrive-s3/",
"description": "The Pendrive S3 is a USB flash drive that can be used to store data or to run a custom firmware on a ThingPulse device."
}