Skip to content

Commit

Permalink
feat: update to Angular's control flow and standalone components
Browse files Browse the repository at this point in the history
  • Loading branch information
robsonos committed Jan 13, 2024
1 parent 006c81b commit fdf116d
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 61 deletions.
4 changes: 2 additions & 2 deletions example/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Component } from '@angular/core';

Check warning on line 1 in example/src/app/app.component.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'Component' is defined but never used
import { IonicModule } from '@ionic/angular';
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';

Check warning on line 2 in example/src/app/app.component.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'IonApp' is defined but never used

Check warning on line 2 in example/src/app/app.component.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'IonRouterOutlet' is defined but never used

@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
standalone: true,
imports: [IonicModule],
imports: [IonApp, IonRouterOutlet],
})
export class AppComponent {}

Check warning on line 11 in example/src/app/app.component.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'AppComponent' is defined but never used
4 changes: 2 additions & 2 deletions example/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import type { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'scan',
loadComponent: () => import('./scan/scan.page').then((m) => m.ScanPageComponent),
loadComponent: () => import('./scan/scan.page').then((m) => m.ScanPage),
},
{
path: 'dfu',
loadComponent: () => import('./scan/dfu/dfu.page').then((m) => m.DfuComponent),
loadComponent: () => import('./scan/dfu/dfu.page').then((m) => m.DfuPage),
},
{
path: '',
Expand Down
20 changes: 9 additions & 11 deletions example/src/app/scan/dfu/dfu.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ion-content>
<ion-list>
<ion-list-header>
<ion-card-title>DFU state</ion-card-title>
<ion-label>DFU state</ion-label>
</ion-list-header>
<ion-item>
State: {{ update?.state ?? 'unknown' }} <br />
Expand All @@ -22,16 +22,15 @@
Duration: {{ (update?.data?.duration ?? 0) | number }} <br />
Remaining time: {{ (update?.data?.remainingTime ?? 0) | number }} <br />
</ion-item>
<ion-progress-bar
*ngIf="update && update.data && update.data.percent"
[value]="update.data.percent / 100"
></ion-progress-bar>
@if(update && update.data && update.data.percent){
<ion-progress-bar [value]="update.data.percent / 100"></ion-progress-bar>
}
</ion-list>

<ion-button expand="block" (click)="pickFile()">Pick Files</ion-button>
<ion-button expand="block" (click)="updateFirmware()" [disabled]="!file">Update Firmware</ion-button>

<ion-list inset *ngIf="file; else noFile">
@if(file){
<ion-list inset>
<ion-item>
<ion-label>
<ion-text>{{ file.name }}</ion-text><br />
Expand All @@ -42,8 +41,7 @@
</div>
</ion-item>
</ion-list>

<ng-template #noFile>
<ion-note color="medium" class="ion-margin-horizontal">No file selected.</ion-note>
</ng-template>
} @else {
<ion-note color="medium" class="ion-margin-horizontal">No file selected.</ion-note>
}
</ion-content>
51 changes: 41 additions & 10 deletions example/src/app/scan/dfu/dfu.page.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { CommonModule } from '@angular/common';
import { AsyncPipe, DecimalPipe } from '@angular/common';

Check warning on line 2 in example/src/app/scan/dfu/dfu.page.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'AsyncPipe' is defined but never used

Check warning on line 2 in example/src/app/scan/dfu/dfu.page.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'DecimalPipe' is defined but never used
import { Component, Inject, NgZone } from '@angular/core';

Check warning on line 3 in example/src/app/scan/dfu/dfu.page.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'Component' is defined but never used

Check warning on line 3 in example/src/app/scan/dfu/dfu.page.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'Inject' is defined but never used
import { Router } from '@angular/router';
import { type PluginResultError } from '@capacitor/core';
import { Directory, Filesystem } from '@capacitor/filesystem';
import { type BleDevice, type ScanResult } from '@capacitor-community/bluetooth-le';
import { FilePicker, type PickedFile } from '@capawesome/capacitor-file-picker';
import { IonicModule, Platform } from '@ionic/angular';
import {
IonHeader,

Check warning on line 10 in example/src/app/scan/dfu/dfu.page.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'IonHeader' is defined but never used
IonToolbar,

Check warning on line 11 in example/src/app/scan/dfu/dfu.page.ts

View workflow job for this annotation

GitHub Actions / verify-ios

'IonToolbar' is defined but never used
IonTitle,
IonContent,
IonList,
IonListHeader,
IonItem,
IonButton,
IonButtons,
IonBackButton,
IonLabel,
IonText,
IonNote,
IonProgressBar,
IonSegment,
IonSegmentButton,
} from '@ionic/angular/standalone';
import { NordicDfu, type DfuUpdateOptions, DfuOptions, DfuUpdate } from 'capacitor-community-nordic-dfu';

import { ToastService } from '../../services/toast.service';
Expand All @@ -16,19 +33,33 @@ import { ToastService } from '../../services/toast.service';
templateUrl: 'dfu.page.html',
styleUrls: ['dfu.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule],
imports: [
AsyncPipe,
DecimalPipe,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonList,
IonListHeader,
IonItem,
IonButton,
IonButtons,
IonBackButton,
IonLabel,
IonText,
IonNote,
IonProgressBar,
IonSegment,
IonSegmentButton,
],
})
export class DfuComponent {
export class DfuPage {
public device!: BleDevice;
public file!: PickedFile | undefined;
public update: DfuUpdate | undefined;

constructor(
@Inject(NgZone) private ngZone: NgZone,
public platform: Platform,
private router: Router,
private toastService: ToastService
) {
constructor(@Inject(NgZone) private ngZone: NgZone, private router: Router, private toastService: ToastService) {
const navigation = this.router.getCurrentNavigation();

if (!navigation) {
Expand Down
31 changes: 21 additions & 10 deletions example/src/app/scan/scan.page.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
<ion-header>
<ion-toolbar>
<ion-toolbar color="secondary">
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Scan</ion-title>
<ion-progress-bar *ngIf="scanProgress > 0" [value]="scanProgress"></ion-progress-bar>
@if(scanProgress > 0){
<ion-progress-bar [value]="scanProgress"></ion-progress-bar>
}
</ion-toolbar>
</ion-header>

<ion-content>
<ion-button expand="block" [disabled]="bluetoothIsScanning" (click)="scanForBluetoothDevices()">
{{ bluetoothIsScanning ? "Scanning" : "Scan" }}
</ion-button>
<ion-refresher slot="fixed" #scanRefresher (ionRefresh)="scanForBluetoothDevices($event)">
<ion-refresher-content pullingText="Pull to scan" refreshingText="Scanning..."></ion-refresher-content>
</ion-refresher>

<ion-grid>
<ion-list *ngIf="(scanResults$ | async) as scanResults; else noDevices">
@if(scanResults$ | async; as scanResults){
<ion-list>
@for (scanResult of scanResults; track $index) {
<ion-item
*ngFor="let scanResult of scanResults"
(click)="stopScanForBluetoothDevices()"
[routerLink]="['/dfu']"
[state]="{ device: scanResult}"
lines="full"
button
>
<ion-icon color="primary" slot="start" [src]="getRssiIcon(scanResult.rssi || -90)" size="large"></ion-icon>
<ion-label> {{ scanResult.device.name || 'Unknown' }} </ion-label>
<ion-note slot="end">{{ scanResult.rssi }} db</ion-note>
</ion-item>
</ion-list>
<ng-template #noDevices>
} @empty {
<ion-note color="medium" class="ion-margin-horizontal">No devices found.</ion-note>
</ng-template>
}
</ion-list>

} @else {
<ion-note color="medium" class="ion-margin-horizontal">Swipe down to scan for nearby Bluetooth devices.</ion-note>
}
</ion-grid>
</ion-content>
101 changes: 75 additions & 26 deletions example/src/app/scan/scan.page.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,111 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
import { CommonModule } from '@angular/common';
import { Component, Inject, NgZone } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AsyncPipe } from '@angular/common';
import { Component, Inject, NgZone, ViewChild, type OnDestroy, type OnInit } from '@angular/core';
import { RouterLink } from '@angular/router';
import { BleClient, ScanMode, type ScanResult } from '@capacitor-community/bluetooth-le';
import { IonicModule } from '@ionic/angular';
import {
IonRouterLink,
IonHeader,
IonToolbar,
IonButtons,
IonMenuButton,
IonTitle,
IonContent,
IonGrid,
IonList,
IonItem,
IonItemSliding,
IonItemOption,
IonItemOptions,
IonButton,
IonIcon,
IonLabel,
IonNote,
IonProgressBar,
IonRefresher,
IonRefresherContent,
} from '@ionic/angular/standalone';
import { Subject } from 'rxjs';
import { scan } from 'rxjs/operators';

import { ToastService } from '../services/toast.service';
import { CONSTANTS } from '../shared/constants';

// import { ToastService } from '../../../services/toast.service';
// import { CONSTANTS } from '../../shared/constants';
// import type { RefresherCustomEvent } from '../../shared/custom-event.interface';

@Component({
selector: 'app-scan',
templateUrl: 'scan.page.html',
styleUrls: ['scan.page.scss'],
standalone: true,
imports: [IonicModule, CommonModule, RouterModule],
imports: [
AsyncPipe,
RouterLink,
IonRouterLink,
IonHeader,
IonToolbar,
IonButtons,
IonMenuButton,
IonTitle,
IonContent,
IonGrid,
IonList,
IonItem,
IonItemSliding,
IonItemOption,
IonItemOptions,
IonButton,
IonIcon,
IonLabel,
IonNote,
IonProgressBar,
IonRefresher,
IonRefresherContent,
],
})
export class ScanPageComponent {
bluetoothIsScanning = false;
scanInterval: any;
export class ScanPage implements OnDestroy, OnInit {
/* This is used for stopping the scan before the user leaves the page */
@ViewChild('scanRefresher', { static: false }) scanRefresher!: IonRefresher;

scanInterval!: ReturnType<typeof setInterval>;
scanProgress = 0;
scanResultSubject = new Subject<ScanResult | null>();
scanResults$ = this.scanResultSubject.asObservable().pipe(
scan((acc: ScanResult[], curr: any) => {
scan((acc: ScanResult[], curr: ScanResult | null) => {
if (curr === null) return [];
return [...acc, curr];
}, [])
);

constructor(@Inject(NgZone) private ngZone: NgZone, private toastService: ToastService) {}
constructor(@Inject(NgZone) private ngZone: NgZone, @Inject(ToastService) private toastService: ToastService) {}

async ionViewWillEnter() {
async ngOnInit(): Promise<void> {
try {
await BleClient.initialize();
} catch (error) {
this.toastService.presentErrorToast(`Error initializing bluetooth: ${JSON.stringify(error)}`);
}
}

async scanForBluetoothDevices(): Promise<void> {
async ngOnDestroy(): Promise<void> {
await this.stopScanForBluetoothDevices();
}

async scanForBluetoothDevices(event: any): Promise<void> {
try {
const isEnabled = await BleClient.isEnabled();

if (!isEnabled) {
event.target.complete();
this.toastService.presentErrorToast('Please enable bluetooth');
return;
}
const stopScanAfterMilliSeconds = 10000;
const stopScanAfterMilliSeconds = CONSTANTS.SCAN_DURATION;

this.scanResultSubject.next(null);

this.bluetoothIsScanning = true;
this.toastService.presentInfoToast('Scanning for devices');

await BleClient.requestLEScan(
{
services: [CONSTANTS.UUID128_SVC_NORDIC_UART],
Expand All @@ -67,31 +119,28 @@ export class ScanPageComponent {
}, stopScanAfterMilliSeconds * 0.01);

setTimeout(async () => {
event.target.complete();
this.stopScanForBluetoothDevices();
}, stopScanAfterMilliSeconds);
} catch (error) {
this.bluetoothIsScanning = false;
this.toastService.presentErrorToast(`Error scanning for devices: ${JSON.stringify(error)}`);
}
}

async stopScanForBluetoothDevices(): Promise<void> {
if (this.bluetoothIsScanning) {
await BleClient.stopLEScan();
this.bluetoothIsScanning = false;
this.scanProgress = 0;
clearInterval(this.scanInterval);
this.toastService.presentInfoToast('Scan stopped');
}
await BleClient.stopLEScan();
this.scanRefresher.complete();
this.scanProgress = 0;
clearInterval(this.scanInterval);
}

onBluetoothDeviceFound(result: ScanResult) {
onBluetoothDeviceFound(result: ScanResult): void {
this.ngZone.run(() => {
this.scanResultSubject.next(result);
});
}

getRssiIcon(rssi: number) {
getRssiIcon(rssi: number): string {
if (rssi >= -40) {
return '/assets/svg/wifi_5.svg';
} else if (rssi >= -50) {
Expand Down
1 change: 1 addition & 0 deletions example/src/app/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const CONSTANTS = {
SCAN_DURATION: 10000,
UUID128_SVC_NORDIC_DFU: '00001530-1212-efde-1523-785feabcd123',
UUID128_SVC_NORDIC_UART: '6e400001-b5a3-f393-e0a9-e50e24dcca9e',
UUID128_SVC_NORDIC_UART_CHAR_RXD: '6e400002-b5a3-f393-e0a9-e50e24dcca9e',
Expand Down

0 comments on commit fdf116d

Please sign in to comment.