Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## [Unreleased]

### Added
- Add stream album art to toolbar
- Add rebooting and restarting functions
- Add loading spinners and wifi check in setup
- Add formatting for system info

### Changed
- Replace zeroconf plugin with mdns plugin

### Fixed
- Fix zeroconf issues on Android
- Fix issues during setup

## [0.5.8] - 2026-07-20

### Added
- Add stream pic fallback

### Changed
- Format telemetry info
- Update README

## [0.5.7] - 2026-07-08

### Added
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "beatnik",
"version": "0.6.0",
"version": "0.6.1",
"author": "byrds & bytes gmbh",
"homepage": "https://beatnik.audio",
"scripts": {
Expand All @@ -27,7 +27,7 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@byrds/capacitor-mdns": "^0.2.1",
"@byrds/capacitor-mdns": "^7.0.0",
"@capacitor-community/bluetooth-le": "^7.2.0",
"@capacitor/android": "^7.2.0",
"@capacitor/app": "7.0.2",
Expand Down
34 changes: 11 additions & 23 deletions src/app/components/camilla-dsp/camilla-dsp.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ export class CamillaDspComponent implements OnInit, OnDestroy {
camillaConfigMessage = '';
camillaConfigError = '';

private levelSubscription: Subscription | undefined;

constructor(
private camillaService: CamillaDspService,
private beatnikHardwareService: BeatnikHardwareService
Expand Down Expand Up @@ -73,20 +71,18 @@ export class CamillaDspComponent implements OnInit, OnDestroy {
} else if (message.GetSignalLevels) {
// this.levels = message.GetSignalLevels.value;
// console.log('Signal Levels received:', this.levels);
} else if (message.GetVolume) {
this.currentVolume = message.GetVolume.value;
}
})
);

this.levelSubscription = this.camillaService.signalLevels$.subscribe(levels => {
// Service handles normalization, so we get the raw levels object directly
this.levels = levels;
});

// timeout to allow UI to update
// setTimeout(() => {
// this.getCaptureSignalLevels();

// }, 800);
this.subscriptions.add(
this.camillaService.signalLevels$.subscribe(levels => {
// Service handles normalization, so we get the raw levels object directly
this.levels = levels;
})
);
}

private getHardwareHost(): string | null {
Expand Down Expand Up @@ -253,26 +249,18 @@ export class CamillaDspComponent implements OnInit, OnDestroy {
}

getVolume() {
// Response is applied reactively via the messages$ subscription in ngOnInit
this.camillaService.sendCommand('GetVolume');
this.currentVolume = this.lastMessage.GetVolume?.value ?? this.currentVolume;
console.log('Current volume:', this.currentVolume);
}

setVolume(volume: number) {
this.camillaService.sendCommand('SetVolume', volume);
}

ngOnDestroy() {
// Clean up subscriptions to prevent memory leaks
this.subscriptions.unsubscribe();
// Tell server to stop sending updates
this.camillaService.stopLevelUpdates();
// Disconnect before unsubscribing so no late message/status update fires after teardown
this.camillaService.disconnect();
}

ionViewWillLeave() {
console.log('CamillaDspComponent: Leaving page, cleaning up resources if needed');
this.ngOnDestroy();
this.subscriptions.unsubscribe();
}

updateProcessorParameter(processorKey: string, paramKey: string, newValue: any) {
Expand Down
66 changes: 40 additions & 26 deletions src/app/services/camilla-dsp.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { Subject, Observable, BehaviorSubject, timer, of } from 'rxjs';
import { retryWhen, switchMap, tap, delayWhen, filter, map } from 'rxjs/operators';
import { Subject, Observable, BehaviorSubject, timer, Subscription } from 'rxjs';
import { retry, filter, map, takeUntil } from 'rxjs/operators';

// Defines the possible connection states
export type ConnectionStatus = 'Connected' | 'Connecting' | 'Disconnected' | 'Error';
Expand All @@ -15,8 +15,11 @@ interface CamillaDspCommand {
@Injectable({
providedIn: 'root',
})
export class CamillaDspService {
private socket$!: WebSocketSubject<any>;
export class CamillaDspService implements OnDestroy {
private socket$: WebSocketSubject<any> | null = null;
private pipelineSubscription: Subscription | null = null;
// Emits to cancel any in-flight reconnect attempt (e.g. on manual disconnect)
private readonly stopReconnect$ = new Subject<void>();
private messagesSubject = new Subject<any>();
private connectionStatusSubject = new BehaviorSubject<ConnectionStatus>('Disconnected');
private readonly RECONNECT_INTERVAL_MS = 5000;
Expand All @@ -36,13 +39,15 @@ export class CamillaDspService {

/**
* Establishes a connection to the CamillaDSP WebSocket server.
* Safe to call multiple times - ignored while already connected/connecting/retrying,
* to avoid stacking up duplicate sockets and subscriptions.
* @param url The full WebSocket URL (e.g., 'ws://beatnik-client-amp.local:1234')
*/
public connect(url: string): void {
// if (this.socket$ && !this.socket$.closed) {
// console.log('Already connected.');
// return;
// }
if (this.connectionStatusSubject.value !== 'Disconnected') {
console.log('CamillaDSP already connected/connecting; ignoring duplicate connect() call.');
return;
}

this.connectionStatusSubject.next('Connecting');
console.log(`Connecting to ${url}...`);
Expand All @@ -63,24 +68,21 @@ export class CamillaDspService {
},
});

this.socket$
this.pipelineSubscription = this.socket$
.pipe(
// The retryWhen operator handles reconnection logic
retryWhen(errors =>
errors.pipe(
tap(err => {
console.error(`Connection error: ${err}. Retrying in ${this.RECONNECT_INTERVAL_MS / 1000}s...`);
this.connectionStatusSubject.next('Error');
}),
// Wait for the specified interval before trying to reconnect
delayWhen(() => timer(this.RECONNECT_INTERVAL_MS))
)
)
// Retries indefinitely with a fixed delay; takeUntil below lets disconnect() cancel it
retry({
delay: err => {
console.error(`Connection error: ${err}. Retrying in ${this.RECONNECT_INTERVAL_MS / 1000}s...`);
this.connectionStatusSubject.next('Error');
return timer(this.RECONNECT_INTERVAL_MS);
},
}),
takeUntil(this.stopReconnect$)
)
.subscribe({
next: msg => this.messagesSubject.next(msg), // Forward messages to our subject
error: err => {
// This block is less likely to be hit due to retryWhen, but good for unrecoverable errors
console.error('WebSocket unrecoverable error:', err);
this.connectionStatusSubject.next('Error');
},
Expand All @@ -93,7 +95,7 @@ export class CamillaDspService {
* @param params Optional parameters for the command.
*/
public sendCommand(command: string, params: any = null): void {
if (this.connectionStatusSubject.value !== 'Connected') {
if (this.connectionStatusSubject.value !== 'Connected' || !this.socket$) {
console.warn('Cannot send command while not connected.');
return;
}
Expand Down Expand Up @@ -136,12 +138,24 @@ export class CamillaDspService {
}

/**
* Closes the WebSocket connection gracefully.
* Closes the WebSocket connection gracefully and cancels any pending reconnect attempt.
*/
public disconnect(): void {
if (this.socket$) {
this.stopLevelUpdates();
this.stopLevelUpdates();
this.stopReconnect$.next(); // cancel any in-flight retry delay so it doesn't reconnect afterwards
this.pipelineSubscription?.unsubscribe();
this.pipelineSubscription = null;
if (this.socket$ && !this.socket$.closed) {
this.socket$.complete(); // This will trigger the closeObserver
}
this.socket$ = null;
this.connectionStatusSubject.next('Disconnected');
}

ngOnDestroy(): void {
this.disconnect();
this.messagesSubject.complete();
this.connectionStatusSubject.complete();
this.stopReconnect$.complete();
}
}
Loading