Installation & Setup
Get started with CUSS2 Angular in your project.
1. Install the Package
npm install cuss2-angular
2. Import Services
import {
Cuss2Service,
BarcodeReaderService,
CardReaderService,
CameraService,
BiometricService,
AnnouncementService
} from 'cuss2-angular';
3. Basic Component Setup
import { Component, inject, OnInit } from '@angular/core';
import { Cuss2Service } from 'cuss2-angular';
@Component({
selector: 'app-kiosk',
template: `
CUSS2 Platform Ready
Connecting to CUSS2 platform...
`
})
export class KioskComponent implements OnInit {
private cuss2Service = inject(Cuss2Service);
ngOnInit() {
// Connect to your CUSS2 platform
this.cuss2Service.connect(
'your-client-id',
'your-client-secret',
'wss://your-platform.airline.com/cuss2' // Optional custom URL
);
}
}
Basic Connection Management
Learn how to manage CUSS2 platform connections effectively.
Connection with Error Handling
export class ConnectionService {
private cuss2Service = inject(Cuss2Service);
private announcement = inject(AnnouncementService);
async connectToPlatform() {
try {
const connected = await this.cuss2Service.connect(
'your-client-id',
'your-client-secret',
'wss://platform.airline.com/cuss2' // Optional custom URL
).toPromise();
if (connected) {
console.log('Successfully connected to CUSS2 platform');
this.announcement.say('Platform connected. Kiosk is ready.');
}
} catch (error) {
console.error('Connection failed:', error);
this.handleConnectionError(error);
}
}
private handleConnectionError(error: any) {
// Implement retry logic or offline mode
setTimeout(() => {
console.log('Retrying connection...');
this.connectToPlatform();
}, 5000);
}
// Monitor connection status
ngOnInit() {
this.cuss2Service.connected$.subscribe(connected => {
if (connected) {
this.enableKioskFeatures();
} else {
this.disableKioskFeatures();
this.showOfflineMessage();
}
});
}
}
Component Status Monitoring
Monitor and manage the status of CUSS2 components.
export class ComponentStatusComponent {
private cuss2Service = inject(Cuss2Service);
componentStatus$ = this.cuss2Service.components$.pipe(
map(components => {
return {
barcodeReader: components.find(c => c.deviceType === 'BARCODE_READER'),
cardReader: components.find(c => c.deviceType === 'MSR_READER'),
printer: components.find(c => c.deviceType === 'BOARDING_PASS_PRINTER'),
scale: components.find(c => c.deviceType === 'SCALE')
};
})
);
ngOnInit() {
this.componentStatus$.subscribe(status => {
console.log('Component Status:', status);
// Enable features based on available components
if (status.barcodeReader?.enabled) {
this.enableBarcodeScanning();
}
if (status.cardReader?.enabled) {
this.enablePaymentProcessing();
}
if (!status.printer?.enabled) {
this.showPrinterOfflineWarning();
}
});
}
// Auto-enable disabled components
private autoEnableComponents() {
this.componentStatus$.pipe(
map(status => Object.values(status).filter(c => c && !c.enabled))
).subscribe(disabledComponents => {
disabledComponents.forEach(component => {
if (component) {
this.cuss2Service.enableComponent(component.id).subscribe({
next: () => console.log(`Enabled ${component.deviceType}`),
error: (err) => console.error(`Failed to enable ${component.deviceType}:`, err)
});
}
});
});
}
}
Complete Check-In Flow
Build a complete passenger check-in flow with barcode scanning and boarding pass printing.
export class CheckInFlowComponent implements OnInit {
private barcodeReader = inject(BarcodeReaderService);
private documentReader = inject(DocumentReaderService);
private boardingPassPrinter = inject(BoardingPassPrinterService);
private announcement = inject(AnnouncementService);
currentStep$ =
new BehaviorSubject<'welcome' | 'scan' | 'processing' | 'print' | 'complete'>('welcome');
passengerData$ = new BehaviorSubject(null);
ngOnInit() {
this.setupBarcodeScanning();
this.setupDocumentScanning();
this.startCheckInFlow();
}
private startCheckInFlow() {
this.currentStep$.next('welcome');
this.announcement.say(
'Welcome to check-in. Please scan your boarding pass or place your passport on the reader.'
);
// Enable scanning devices
this.barcodeReader.enable().subscribe();
this.documentReader.enable().subscribe();
}
private setupBarcodeScanning() {
this.barcodeReader.data$.pipe(
filter(data => data?.meta?.messageCode === MessageCodes.DATA_PRESENT),
map(data => data.payload.dataRecords.map(dr => dr.data)),
takeUntil(this.destroy$)
).subscribe(barcodes => {
if (barcodes[0] && this.currentStep$.value === 'scan') {
this.processBoardingPass(barcodes[0]);
}
});
}
private setupDocumentScanning() {
this.documentReader.data$.pipe(
filter(data => data?.meta?.messageCode === MessageCodes.DATA_PRESENT),
map(data => data.payload.dataRecords.map(dr => dr.data)),
takeUntil(this.destroy$)
).subscribe(documentLines => {
if (this.currentStep$.value === 'scan') {
this.processPassport(documentLines);
}
});
}
private async processBoardingPass(barcode: string) {
this.currentStep$.next('processing');
this.announcement.say('Boarding pass detected. Processing...');
try {
// Parse boarding pass data (implementation depends on format)
const passengerData = this.parseBoardingPass(barcode);
// Validate with airline systems
const validatedData = await this.validatePassenger(passengerData);
this.passengerData$.next(validatedData);
this.currentStep$.next('print');
await this.printBoardingPass(validatedData);
} catch (error) {
console.error('Check-in processing failed:', error);
this.announcement.say('Check-in failed. Please see agent for assistance.');
this.resetFlow();
}
}
private async processPassport(documentLines: string[]) {
this.currentStep$.next('processing');
this.announcement.say('Passport detected. Reading information...');
try {
const mrzLines = documentLines.filter(line => this.isMRZLine(line));
const passportData = this.parseMRZ(mrzLines);
// Create check-in record
const passengerData = await this.createCheckInFromPassport(passportData);
this.passengerData$.next(passengerData);
this.currentStep$.next('print');
await this.printBoardingPass(passengerData);
} catch (error) {
console.error('Passport processing failed:', error);
this.announcement.say('Unable to process passport. Please see agent.');
this.resetFlow();
}
}
private async printBoardingPass(passengerData: any) {
try {
// Setup printer with airline assets
await this.boardingPassPrinter.setup([
'PT01AIRLINE_LOGO.png',
'PT02Boarding Pass Template',
'PT03Flight Information Layout'
]).toPromise();
// Print boarding pass with passenger data
await this.boardingPassPrinter.print([
'CP#A#01S#CP#C01#02@@01#03M1THIS IS A BARCODE#04THIS IS A BOARDING PASS#'
]).toPromise();
this.currentStep$.next('complete');
this.announcement.say(
`${passengerData.name}, your boarding pass is ready. ` +
`Flight ${passengerData.flight} departs from gate ${passengerData.gate} ` +
`at ${passengerData.departureTime}. Have a great flight!`
);
// Auto-reset after 10 seconds
setTimeout(() => this.resetFlow(), 10000);
} catch (error) {
console.error('Printing failed:', error);
this.announcement.say('Printing failed. Please see agent for assistance.');
this.resetFlow();
}
}
private resetFlow() {
this.currentStep$.next('welcome');
this.passengerData$.next(null);
setTimeout(() => this.startCheckInFlow(), 3000);
}
private parseBoardingPass(barcode: string) {
// Implementation depends on barcode format (IATA BCBP, etc.)
// Return passenger information
}
private parseMRZ(mrzLines: string[]) {
// Parse Machine Readable Zone from passport
// Return passport data
}
}
Payment Processing
Implement secure payment card processing with the CardReaderService.
export class PaymentComponent {
private cardReader = inject(CardReaderService);
private announcement = inject(AnnouncementService);
paymentState$ = new BehaviorSubject<'idle' | 'waiting' | 'processing' | 'success' | 'failed'>('idle');
amount$ = new BehaviorSubject(0);
ngOnInit() {
this.setupCardReaderEvents();
}
async processPayment(amount: number) {
this.amount$.next(amount);
this.paymentState$.next('waiting');
this.announcement.say(
`Please insert or swipe your payment card. The total amount is ${amount} dollars.`
);
try {
// Enable payment mode
await this.cardReader.enablePayment(true).toPromise();
// Subscribe to payment data
const paymentSubscription = this.cardReader.data$.pipe(
filter(data => data?.meta?.messageCode === MessageCodes.DATA_PRESENT),
take(1)
).subscribe(paymentData => {
const cardTracks = paymentData.payload.dataRecords.map(dr => dr.data);
this.processPaymentCard(cardTracks, amount);
});
// Start payment read with 30 second timeout
await this.cardReader.readPayment(30000).toPromise();
} catch (error) {
console.error('Payment initiation failed:', error);
this.paymentFailed('Unable to initialize payment. Please try again.');
}
}
private async processPaymentCard(tracks: string[], amount: number) {
this.paymentState$.next('processing');
this.announcement.say('Processing payment...');
try {
// Extract card information
const cardInfo = this.parseCardTracks(tracks);
if (!cardInfo.isValid) {
throw new Error('Invalid card data');
}
// Process payment through payment gateway
const paymentResult = await this.submitPaymentToGateway(cardInfo, amount);
if (paymentResult.approved) {
this.paymentSuccess(paymentResult);
} else {
this.paymentFailed(paymentResult.message || 'Payment declined');
}
} catch (error) {
console.error('Payment processing error:', error);
this.paymentFailed('Payment processing failed. Please try again.');
} finally {
// Always disable payment mode
await this.cardReader.enablePayment(false).toPromise();
}
}
private paymentSuccess(result: any) {
this.paymentState$.next('success');
this.announcement.say(
`Payment approved. Transaction ID: ${result.transactionId}. Thank you!`
);
// Auto-reset after showing success
setTimeout(() => {
this.paymentState$.next('idle');
}, 5000);
}
private paymentFailed(message: string) {
this.paymentState$.next('failed');
this.announcement.say(message);
// Reset after error message
setTimeout(() => {
this.paymentState$.next('idle');
}, 3000);
}
private parseCardTracks(tracks: string[]) {
// Parse card track data
// Extract card number, expiry, holder name, etc.
// Return structured card information
const track1 = tracks[0] || '';
const track2 = tracks[1] || '';
// Basic validation
const isValid = track1.length > 0 || track2.length > 0;
return {
isValid,
cardNumber: this.extractCardNumber(track2),
expiryDate: this.extractExpiryDate(track2),
cardHolderName: this.extractCardHolderName(track1),
serviceCode: this.extractServiceCode(track2)
};
}
private async submitPaymentToGateway(cardInfo: any, amount: number) {
// Implement payment gateway integration
// This would typically call your payment service API
const paymentRequest = {
cardNumber: cardInfo.cardNumber,
expiryDate: cardInfo.expiryDate,
amount: amount,
currency: 'USD',
merchantId: 'YOUR_MERCHANT_ID'
};
// Example API call
const response = await fetch('/api/payment/process', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(paymentRequest)
});
return response.json();
}
// Card parsing helper methods
private extractCardNumber(track2: string): string {
// Extract card number from track 2 data
const match = track2.match(/^(\d{13,19})/);
return match ? match[1] : '';
}
private extractExpiryDate(track2: string): string {
// Extract expiry date from track 2 data
const match = track2.match(/=(\d{4})/);
return match ? match[1] : '';
}
private extractCardHolderName(track1: string): string {
// Extract cardholder name from track 1 data
const match = track1.match(/\^([^=]+)/);
return match ? match[1].replace(/\//g, ' ').trim() : '';
}
private extractServiceCode(track2: string): string {
// Extract service code from track 2 data
const match = track2.match(/=\d{4}(\d{3})/);
return match ? match[1] : '';
}
}
Baggage Handling
Implement baggage weighing and tag printing with weight validation.
export class BaggageHandlingComponent {
private scale = inject(ScaleService);
private bagTagPrinter = inject(BagTagPrinterService);
private announcement = inject(AnnouncementService);
baggageState$ =
new BehaviorSubject<'idle' | 'weighing' | 'overweight' | 'printing' | 'complete'>('idle');
currentWeight$ = new BehaviorSubject(0);
maxWeight = 50; // kg
passenger: any;
ngOnInit() {
this.setupScaleMonitoring();
}
async startBaggageProcess(passenger: any) {
this.passenger = passenger;
this.baggageState$.next('weighing');
this.announcement.say(
`${passenger.name}, please place your bag on the scale for weighing.`
);
// Enable scale
await this.scale.enable().toPromise();
// Wait for stable weight reading
this.waitForStableWeight();
}
private setupScaleMonitoring() {
// Monitor real-time weight changes with proper data structure
this.scale.data$.pipe(
filter(data => data?.meta?.messageCode === MessageCodes.DATA_PRESENT),
map(data => {
const weightData = data.payload as WeightData;
const grossWeight = weightData.weight.grossWeight;
// Convert to kg if needed
const weightKg = grossWeight.unit === 'METRIC_KILOGRAMM' ?
grossWeight.weight :
grossWeight.weight * 0.453592; // Convert pounds to kg
return grossWeight.stable ? weightKg : 0;
}),
// Only emit when weight changes significantly
distinctUntilChanged((prev, curr) => Math.abs(prev - curr) < 0.1),
takeUntil(this.destroy$)
).subscribe(weight => {
this.currentWeight$.next(weight);
this.updateWeightDisplay(weight);
});
}
private async waitForStableWeight() {
let stableCount = 0;
let lastWeight = 0;
const requiredStableReadings = 5;
const stabilityThreshold = 0.2; // kg
const subscription = this.currentWeight$.pipe(
filter(weight => weight > 0), // Only consider non-zero weights
takeUntil(this.destroy$)
).subscribe(weight => {
const weightDiff = Math.abs(weight - lastWeight);
if (weightDiff <= stabilityThreshold) {
stableCount++;
if (stableCount >= requiredStableReadings) {
subscription.unsubscribe();
this.processStableWeight(weight);
}
} else {
stableCount = 0;
}
lastWeight = weight;
});
// Timeout after 30 seconds if no stable reading
setTimeout(() => {
subscription.unsubscribe();
if (this.baggageState$.value === 'weighing') {
this.announcement.say('Unable to get stable weight reading. Please try again.');
this.resetBaggageProcess();
}
}, 30000);
}
private async processStableWeight(weight: number) {
console.log(`Stable weight detected: ${weight}kg`);
if (weight > this.maxWeight) {
this.handleOverweight(weight);
} else {
await this.printBagTag(weight);
}
}
private handleOverweight(weight: number) {
this.baggageState$.next('overweight');
const excessWeight = weight - this.maxWeight;
this.announcement.say(
`Your bag is overweight. It weighs ${weight} kilograms, ` +
`which exceeds the ${this.maxWeight} kilogram limit by ${excessWeight.toFixed(1)} kilograms. ` +
`Please remove some items or see an agent for excess baggage fees.`
);
// Allow passenger to adjust and try again
setTimeout(() => {
this.announcement.say('Please adjust your bag and place it on the scale again.');
this.baggageState$.next('weighing');
this.waitForStableWeight();
}, 10000);
}
private async printBagTag(weight: number) {
this.baggageState$.next('printing');
this.announcement.say(`Bag weight accepted: ${weight} kilograms. Printing bag tag...`);
try {
// Setup bag tag printer with assets
const companyLogo = 'LT0146940A020101000000001D01630064006400000000000000000000' +
'000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF...';
const assets = 'BTT0801~J 500262=#01C0M5493450304#02C0M5493450304#03B1MA020250541=06' +
'#04B1MK200464141=06#05L0 A258250000#\n' + companyLogo;
await this.bagTagPrinter.setup([assets]).toPromise();
// Generate unique bag tag number
const bagTagNumber = this.generateBagTagNumber();
// Print bag tag with passenger and weight information
const coupon = 'BTP080101#01THIS IS A#02BAG TAG#03123#04456#0501#';
await this.bagTagPrinter.print([coupon]).toPromise();
this.baggageState$.next('complete');
this.announcement.say(
`Bag tag printed successfully. Your bag tag number is ${bagTagNumber}. ` +
`Please attach the tag to your bag and proceed to bag drop.`
);
// Save baggage information
await this.saveBaggageInfo(bagTagNumber, weight);
// Auto-reset after success
setTimeout(() => this.resetBaggageProcess(), 8000);
} catch (error) {
console.error('Bag tag printing failed:', error);
this.announcement.say('Bag tag printing failed. Please see an agent for assistance.');
this.resetBaggageProcess();
}
}
private generateBagTagNumber(): string {
// Generate airline-specific bag tag number
const airline = this.passenger.airline || 'AA';
const timestamp = Date.now().toString().slice(-6);
return `${airline}${timestamp}`;
}
private async saveBaggageInfo(tagNumber: string, weight: number) {
// Save baggage information to airline systems
const baggageInfo = {
pnr: this.passenger.pnr,
tagNumber,
weight,
timestamp: new Date().toISOString(),
route: this.passenger.route
};
try {
await fetch('/api/baggage/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(baggageInfo)
});
} catch (error) {
console.error('Failed to save baggage info:', error);
}
}
private updateWeightDisplay(weight: number) {
// Update UI with current weight
const weightElement = document.getElementById('weight-display');
if (weightElement) {
weightElement.textContent = `${weight.toFixed(1)} kg`;
// Add visual feedback for weight status
if (weight > this.maxWeight) {
weightElement.className = 'weight-display overweight';
} else if (weight > this.maxWeight * 0.9) {
weightElement.className = 'weight-display warning';
} else {
weightElement.className = 'weight-display normal';
}
}
}
private resetBaggageProcess() {
this.baggageState$.next('idle');
this.currentWeight$.next(0);
this.passenger = null;
}
}
Camera Capture
Implement passenger photo capture for documentation and verification purposes.
export class PhotoCaptureComponent {
private camera = inject(CameraService);
private announcement = inject(AnnouncementService);
captureState$ = new BehaviorSubject<'idle' | 'preparing' | 'ready' | 'capturing' | 'complete'>('idle');
capturedPhoto$ = new BehaviorSubject(null);
ngOnInit() {
this.setupCameraReadiness();
}
private setupCameraReadiness() {
// Monitor camera ready status
this.camera.onReady$.subscribe(ready => {
if (ready) {
this.captureState$.next('ready');
this.announcement.say('Camera is ready. Please look at the camera.');
}
});
// Monitor captured photos using onRead$
this.camera.onRead$.subscribe(photos => {
if (photos && photos.length > 0 && photos[0]) {
this.capturedPhoto$.next(photos[0]);
this.captureState$.next('complete');
this.announcement.say('Photo captured successfully.');
}
});
}
async startPhotoCapture() {
this.captureState$.next('preparing');
this.announcement.say('Preparing camera for photo capture.');
try {
// Enable camera
await this.camera.enable().toPromise();
// Wait for camera to be ready
this.captureState$.next('ready');
this.announcement.say('Please position yourself in front of the camera.');
// Countdown before capture
await this.countdownToCapture();
// Capture photo with 10 second timeout
this.captureState$.next('capturing');
const photos = await this.camera.read(10000).toPromise();
if (photos && photos.length > 0) {
console.log('Photo captured successfully:', photos);
this.savePhoto(photos[0]);
}
} catch (error) {
console.error('Photo capture failed:', error);
this.announcement.say('Photo capture failed. Please try again.');
this.captureState$.next('idle');
}
}
private async countdownToCapture() {
const countdown = [3, 2, 1];
for (const count of countdown) {
await this.announcement.say(\`\${count}\`).toPromise();
await new Promise(resolve => setTimeout(resolve, 1000));
}
await this.announcement.say('Smile!').toPromise();
}
private async savePhoto(photoData: any) {
// Save photo to backend
try {
await fetch('/api/passenger/photo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
photoData: photoData,
timestamp: new Date().toISOString()
})
});
this.announcement.say('Photo saved successfully.');
} catch (error) {
console.error('Failed to save photo:', error);
}
}
retakePhoto() {
this.captureState$.next('idle');
this.capturedPhoto$.next(null);
this.startPhotoCapture();
}
}
Biometric Verification
Implement biometric verification for secure passenger identification.
export class BiometricVerificationComponent {
private biometric = inject(BiometricService);
private announcement = inject(AnnouncementService);
verificationState$ =
new BehaviorSubject<'idle' | 'ready' | 'capturing' | 'verifying' | 'success' | 'failed'>('idle');
verificationResult$ = new BehaviorSubject(null);
ngOnInit() {
this.setupBiometricReadiness();
}
private setupBiometricReadiness() {
// Monitor biometric device ready status
this.biometric.onReady$.subscribe(ready => {
if (ready) {
this.verificationState$.next('ready');
this.announcement.say('Biometric scanner is ready.');
}
});
// Monitor biometric data using onRead$
this.biometric.onRead$.subscribe(biometricData => {
if (biometricData && biometricData.length > 0) {
this.verificationState$.next('verifying');
this.performVerification(biometricData);
}
});
}
async startBiometricVerification(verificationType: 'fingerprint' | 'face') {
this.verificationState$.next('ready');
const prompts = {
fingerprint: 'Please place your finger on the scanner.',
face: 'Please look directly at the camera for face verification.'
};
this.announcement.say(prompts[verificationType]);
try {
// Enable biometric device
await this.biometric.enable().toPromise();
// Start capture with 30 second timeout
this.verificationState$.next('capturing');
const biometricData = await this.biometric.read(30000).toPromise();
if (biometricData && biometricData.length > 0) {
console.log('Biometric data captured:', biometricData);
await this.performVerification(biometricData);
}
} catch (error) {
console.error('Biometric capture failed:', error);
this.verificationState$.next('failed');
this.announcement.say(
'Biometric verification failed. Please try again or see an agent for assistance.'
);
}
}
private async performVerification(biometricData: string[]) {
this.verificationState$.next('verifying');
this.announcement.say('Verifying your identity. Please wait.');
try {
// Send biometric data to verification service
const response = await fetch('/api/biometric/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
biometricData: biometricData[0],
timestamp: new Date().toISOString()
})
});
const result = await response.json();
if (result.verified) {
this.verificationState$.next('success');
this.verificationResult$.next(result);
this.announcement.say(
\`Verification successful. Welcome, \${result.passengerName}.\`
);
// Proceed with check-in or boarding
this.proceedWithVerifiedPassenger(result);
} else {
this.verificationState$.next('failed');
this.announcement.say(
'Verification failed. Please try again or see an agent for assistance.'
);
}
} catch (error) {
console.error('Verification request failed:', error);
this.verificationState$.next('failed');
this.announcement.say('Unable to verify identity. Please see an agent.');
} finally {
// Disable biometric device
await this.biometric.disable().toPromise();
}
}
private proceedWithVerifiedPassenger(result: any) {
// Continue with passenger flow
console.log('Proceeding with verified passenger:', result.passengerName);
// Navigate to next step in the flow
}
async startMultiFactorVerification() {
this.announcement.say(
'Starting multi-factor verification. Please complete both biometric and document verification.'
);
try {
// First, capture biometric data
await this.startBiometricVerification('face');
// Wait for success
const verificationSuccess = await this.verificationState$.pipe(
filter(state => state === 'success' || state === 'failed'),
take(1)
).toPromise();
if (verificationSuccess === 'success') {
this.announcement.say(
'Biometric verification complete. Please scan your travel document.'
);
// Proceed to document scanning
}
} catch (error) {
console.error('Multi-factor verification failed:', error);
this.announcement.say('Verification failed. Please see an agent.');
}
}
}
Accessibility Support
Implement comprehensive accessibility features with headset and keypad navigation.
export class AccessibilityKioskComponent implements OnInit {
private headset = inject(HeadsetService);
private keypad = inject(KeypadService);
private announcement = inject(AnnouncementService);
accessibilityMode$ = new BehaviorSubject(false);
currentFocus$ = new BehaviorSubject(0);
menuItems = [
{ id: 'check-in', label: 'Check In', action: () => this.startCheckIn() },
{ id: 'flight-info', label: 'Flight Information', action: () => this.showFlightInfo() },
{ id: 'help', label: 'Help and Assistance', action: () => this.getHelp() },
{ id: 'language', label: 'Change Language', action: () => this.changeLanguage() }
];
ngOnInit() {
this.setupAccessibilityDetection();
this.setupKeypadNavigation();
this.setupVoiceGuidance();
}
private setupAccessibilityDetection() {
// Monitor headset connection
this.headset.component$.pipe(
map(component => component?.enabled || false),
distinctUntilChanged()
).subscribe(headsetAvailable => {
if (headsetAvailable) {
this.enableAccessibilityMode();
} else {
this.disableAccessibilityMode();
}
});
// Listen for headset events
this.headset.events$.subscribe(event => {
switch (event.type) {
case 'connected':
this.onHeadsetConnected();
break;
case 'disconnected':
this.onHeadsetDisconnected();
break;
}
});
}
private async enableAccessibilityMode() {
console.log('Enabling accessibility mode');
try {
// Enable headset
await this.headset.enable().toPromise();
// Enable keypad for navigation
await this.keypad.enable().toPromise();
this.accessibilityMode$.next(true);
// Provide welcome message
await this.announcement.say(
'Accessibility mode enabled. Audio guidance is now available. ' +
'Use the up and down arrow keys to navigate options. ' +
'Press enter to select. Press the help key at any time for assistance.'
).toPromise();
// Start with first menu item
this.currentFocus$.next(0);
this.announceCurrentOption();
} catch (error) {
console.error('Failed to enable accessibility mode:', error);
}
}
private disableAccessibilityMode() {
console.log('Disabling accessibility mode');
this.accessibilityMode$.next(false);
// Switch to visual-only interface
}
private setupKeypadNavigation() {
this.keypad.keypadData$.pipe(
filter(() => this.accessibilityMode$.value),
takeUntil(this.destroy$)
).subscribe((keypad: KeypadData) => {
this.handleKeypadInput(keypad);
});
}
private handleKeypadInput(keypad: KeypadData) {
const currentFocus = this.currentFocus$.value;
if (keypad.UP) {
this.navigateUp();
} else if (keypad.DOWN) {
this.navigateDown();
} else if (keypad.ENTER) {
this.selectCurrentOption();
} else if (keypad.HOME) {
this.goToMainMenu();
} else if (keypad.HELP) {
this.provideHelp();
} else if (keypad.VOLUMEUP) {
this.adjustVolume(1);
} else if (keypad.VOLUMEDOWN) {
this.adjustVolume(-1);
}
}
private navigateUp() {
const current = this.currentFocus$.value;
const newFocus = current > 0 ? current - 1 : this.menuItems.length - 1;
this.currentFocus$.next(newFocus);
this.announceCurrentOption();
}
private navigateDown() {
const current = this.currentFocus$.value;
const newFocus = current < this.menuItems.length - 1 ? current + 1 : 0;
this.currentFocus$.next(newFocus);
this.announceCurrentOption();
}
private selectCurrentOption() {
const currentIndex = this.currentFocus$.value;
const selectedItem = this.menuItems[currentIndex];
if (selectedItem) {
this.announcement.say(`Selected: ${selectedItem.label}`).subscribe(() => {
selectedItem.action();
});
}
}
private announceCurrentOption() {
const currentIndex = this.currentFocus$.value;
const currentItem = this.menuItems[currentIndex];
if (currentItem) {
const position = `Option ${currentIndex + 1} of ${this.menuItems.length}`;
this.announcement.say(`${position}: ${currentItem.label}`);
}
}
private provideHelp() {
const helpText = `
You are using the accessible kiosk interface.
Current options: Check in, Flight information, Help, and Language settings.
Use up and down arrows to navigate between options.
Press enter to select an option.
Press home to return to the main menu.
Press volume up or down to adjust audio volume.
Press help at any time to hear this message again.
`;
this.announcement.say(helpText);
}
private goToMainMenu() {
this.currentFocus$.next(0);
this.announcement.say('Returning to main menu').pipe(
delay(500)
).subscribe(() => {
this.announceCurrentOption();
});
}
private adjustVolume(direction: number) {
// Implement volume adjustment
const action = direction > 0 ? 'increased' : 'decreased';
this.announcement.say(`Volume ${action}`);
}
private setupVoiceGuidance() {
// Provide contextual voice guidance throughout the process
this.accessibilityMode$.pipe(
switchMap(enabled => {
if (enabled) {
return this.cuss2Service.components$.pipe(
map(components => {
return {
barcodeReader: components.find(c => c.deviceType === 'BARCODE_READER')?.enabled,
cardReader: components.find(c => c.deviceType === 'MSR_READER')?.enabled,
printer: components.find(c => c.deviceType === 'BOARDING_PASS_PRINTER')?.enabled
};
})
);
}
return of(null);
})
).subscribe(status => {
if (status) {
this.announceAvailableServices(status);
}
});
}
private announceAvailableServices(status: any) {
const availableServices = [];
if (status.barcodeReader) availableServices.push('barcode scanning');
if (status.cardReader) availableServices.push('payment processing');
if (status.printer) availableServices.push('boarding pass printing');
if (availableServices.length > 0) {
const servicesText = availableServices.join(', ');
this.announcement.say(`Available services: ${servicesText}`);
}
}
// Action methods
private startCheckIn() {
this.announcement.say(
'Starting check-in process. You can scan your boarding pass, ' +
'scan a QR code from your phone, or place your passport on the document reader.'
);
// Navigate to check-in component
}
private showFlightInfo() {
this.announcement.say('Loading flight information');
// Navigate to flight info component
}
private getHelp() {
this.announcement.say(
'For additional assistance, please approach the service desk ' +
'or press the call button to speak with an agent.'
);
}
private changeLanguage() {
const languages = [
{ code: 'en-US', name: 'English' },
{ code: 'es-ES', name: 'Spanish' },
{ code: 'fr-FR', name: 'French' }
];
// Implement language selection with voice prompts
this.announcement.say('Language options: English, Spanish, French. Use arrow keys to select.');
}
private onHeadsetConnected() {
this.announcement.say(
'Headset connected. Welcome to accessible check-in. Audio guidance is now active.'
);
}
private onHeadsetDisconnected() {
console.log('Headset disconnected - accessibility mode will continue with speakers');
}
}
Error Handling Best Practices
Implement robust error handling for production CUSS2 applications.
export class ErrorHandlingService {
private cuss2Service = inject(Cuss2Service);
private announcement = inject(AnnouncementService);
// Centralized error handling with retry logic
handleServiceError(
operation: () => Observable,
serviceName: string,
maxRetries: number = 3,
retryDelay: number = 2000
): Observable {
return operation().pipe(
retry({
count: maxRetries,
delay: (error, retryCount) => {
console.warn(`${serviceName} failed (attempt ${retryCount}/${maxRetries}):`, error);
// Exponential backoff
const delay = retryDelay * Math.pow(2, retryCount - 1);
return timer(delay);
}
}),
catchError(error => {
console.error(`${serviceName} failed after ${maxRetries} retries:`, error);
return this.handleFinalError(error, serviceName);
})
);
}
private handleFinalError(error: any, serviceName: string): Observable {
let userMessage = 'A technical error occurred. Please try again.';
// Provide specific error messages based on error type
if (error.code === 'TIMEOUT') {
userMessage = `${serviceName} timeout. Please ensure the device is ready and try again.`;
} else if (error.code === 'DEVICE_NOT_FOUND') {
userMessage = `${serviceName} is not available. Please see an agent for assistance.`;
} else if (error.code === 'CONNECTION_LOST') {
userMessage = 'Connection to the platform was lost. Reconnecting...';
this.attemptReconnection();
}
// Announce error to user
this.announcement.say(userMessage);
// Throw error for component to handle
return throwError(() => new Error(userMessage));
}
private attemptReconnection() {
this.cuss2Service.connected$.pipe(
filter(connected => !connected),
take(1),
switchMap(() => timer(3000)),
switchMap(() => this.cuss2Service.connect(
'your-client-id',
'your-client-secret'
))
).subscribe({
next: (connected) => {
if (connected) {
this.announcement.say('Connection restored');
}
},
error: () => {
this.announcement.say('Unable to reconnect. Please see an agent.');
}
});
}
// Device-specific error handling
handleBarcodeReaderError(error: any): void {
if (error.message?.includes('timeout')) {
this.announcement.say(
'Barcode scanning timed out. Please ensure your code is clearly visible and try again.'
);
} else if (error.message?.includes('invalid')) {
this.announcement.say(
'Unable to read barcode. Please ensure the code is not damaged and try again.'
);
} else {
this.announcement.say('Barcode scanner error. Please try again or see an agent.');
}
}
handleCardReaderError(error: any): void {
if (error.message?.includes('payment')) {
this.announcement.say(
'Payment card could not be processed. Please check your card and try again.'
);
} else if (error.message?.includes('track')) {
this.announcement.say(
'Card could not be read. Please clean your card and swipe again slowly.'
);
} else {
this.announcement.say('Card reader error. Please try again or use a different payment method.');
}
}
handlePrinterError(error: any): void {
if (error.message?.includes('paper')) {
this.announcement.say('Printer is out of paper. Please see an agent for assistance.');
} else if (error.message?.includes('jam')) {
this.announcement.say('Printer jam detected. Please see an agent for assistance.');
} else {
this.announcement.say('Printing failed. Please see an agent to get your documents.');
}
}
}
Performance Optimization
Tips for optimizing performance in CUSS2 Angular applications.
1. Lazy Load Services
Only inject services when needed to reduce initial bundle size.
// Instead of injecting all services at once
@Component({...})
export class KioskComponent {
// Lazy inject services only when needed
private async enableBarcodeScanning() {
const { BarcodeReaderService } = await import('cuss2-angular');
const barcodeReader = this.injector.get(BarcodeReaderService);
return barcodeReader.enable();
}
}
2. Use OnPush Change Detection
Optimize change detection for better performance.
@Component({
selector: 'app-scanner',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
{{ scanResult$ | async }}
`
})
export class ScannerComponent {
scanResult$ = this.barcodeReader.data$.pipe(
filter(data => data?.meta?.messageCode === MessageCodes.DATA_PRESENT),
map(data => data.payload.dataRecords.map(dr => dr.data)[0])
);
}
3. Unsubscribe Properly
Always unsubscribe to prevent memory leaks.
export class BaseKioskComponent implements OnDestroy {
protected destroy$ = new Subject();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// Use takeUntil in all subscriptions
setupSubscriptions() {
this.barcodeReader.data$.pipe(
takeUntil(this.destroy$)
).subscribe(data => {
// Handle data
});
}
}
4. Debounce Rapid Events
Debounce rapid device events to prevent UI flooding.
// Debounce scale readings for stability
this.scale.data$.pipe(
debounceTime(500), // Wait 500ms between readings
distinctUntilChanged((prev, curr) =>
Math.abs(prev - curr) < 0.1 // Only emit if weight changed significantly
)
).subscribe(weight => {
this.updateWeightDisplay(weight);
});
Testing Strategies
Comprehensive testing approaches for CUSS2 Angular applications.
Unit Testing Services
describe('BarcodeReaderService', () => {
let service: BarcodeReaderService;
let mockCuss2Service: jasmine.SpyObj;
beforeEach(() => {
const spy = jasmine.createSpyObj('Cuss2Service', ['enableComponent', 'getInstance']);
TestBed.configureTestingModule({
providers: [
BarcodeReaderService,
{ provide: Cuss2Service, useValue: spy }
]
});
service = TestBed.inject(BarcodeReaderService);
mockCuss2Service = TestBed.inject(Cuss2Service) as jasmine.SpyObj;
});
it('should emit data when barcode is scanned', fakeAsync(() => {
const mockData: PlatformData = {
meta: { messageCode: MessageCodes.DATA_PRESENT },
payload: {
dataRecords: [{ data: 'TEST123456789' }]
}
};
let receivedData: PlatformData | undefined;
service.data$.subscribe(data => receivedData = data);
// Simulate barcode scan
service['dataSubject'].next(mockData);
tick();
expect(receivedData).toEqual(mockData);
}));
it('should enable barcode reader', () => {
const mockResponse: PlatformData = { meta: { messageCode: MessageCodes.SUCCESS } };
mockCuss2Service.enableComponent.and.returnValue(of(mockResponse));
service.enable().subscribe(response => {
expect(response).toEqual(mockResponse);
});
expect(mockCuss2Service.enableComponent).toHaveBeenCalled();
});
});
Integration Testing
describe('CheckInFlow Integration', () => {
let component: CheckInFlowComponent;
let fixture: ComponentFixture;
let mockServices: {
barcodeReader: jasmine.SpyObj;
announcement: jasmine.SpyObj;
printer: jasmine.SpyObj;
};
beforeEach(() => {
// Create service mocks
mockServices = {
barcodeReader: jasmine.createSpyObj('BarcodeReaderService', ['enable', 'data$']),
announcement: jasmine.createSpyObj('AnnouncementService', ['say']),
printer: jasmine.createSpyObj('BoardingPassPrinterService', ['setup', 'print'])
};
TestBed.configureTestingModule({
declarations: [CheckInFlowComponent],
providers: [
{ provide: BarcodeReaderService, useValue: mockServices.barcodeReader },
{ provide: AnnouncementService, useValue: mockServices.announcement },
{ provide: BoardingPassPrinterService, useValue: mockServices.printer }
]
});
fixture = TestBed.createComponent(CheckInFlowComponent);
component = fixture.componentInstance;
});
it('should complete check-in flow when barcode is scanned', fakeAsync(() => {
// Setup service responses
mockServices.barcodeReader.enable.and.returnValue(of({}));
mockServices.announcement.say.and.returnValue(of({}));
mockServices.printer.setup.and.returnValue(of({}));
mockServices.printer.print.and.returnValue(of({}));
// Mock barcode data stream
const barcodeData$ = new Subject();
mockServices.barcodeReader.data$ = barcodeData$;
// Start check-in
component.startCheckInFlow();
tick();
// Simulate barcode scan
barcodeData$.next({
meta: { messageCode: MessageCodes.DATA_PRESENT },
payload: {
dataRecords: [{ data: 'M1DOE/JOHN UA123 JFKLAX 001F003A0025' }]
}
});
tick();
// Verify flow completion
expect(mockServices.printer.print).toHaveBeenCalled();
expect(component.currentStep$.value).toBe('complete');
}));
});
E2E Testing with Mock Platform
describe('Kiosk E2E Tests', () => {
let page: AppPage;
let mockPlatform: MockCuss2Platform;
beforeEach(() => {
page = new AppPage();
mockPlatform = new MockCuss2Platform();
mockPlatform.start();
});
afterEach(() => {
mockPlatform.stop();
});
it('should complete passenger check-in', async () => {
await page.navigateTo();
await page.waitForConnection();
// Simulate barcode scan
await mockPlatform.simulateBarccodeScan('M1DOE/JOHN...');
// Verify UI updates
expect(await page.getStepText()).toContain('Processing');
// Wait for completion
await page.waitForStep('complete');
expect(await page.getCompletionMessage()).toContain('Check-in complete');
});
});
class MockCuss2Platform {
private server: any;
start() {
// Start mock WebSocket server
this.server = new MockWebSocketServer('ws://localhost:8080');
}
simulateBarccodeScan(data: string) {
this.server.emit('barcode', {
meta: { messageCode: 'DATA_PRESENT' },
payload: { dataRecords: [{ data }] }
});
}
stop() {
this.server?.close();
}
}