diff --git a/firmware/VERSION b/firmware/VERSION index ceff79e..ab553f1 100644 --- a/firmware/VERSION +++ b/firmware/VERSION @@ -1,5 +1,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 1 -PATCHLEVEL = 12 +PATCHLEVEL = 14 VERSION_TWEAK = 0 -EXTRAVERSION = 0 \ No newline at end of file +EXTRAVERSION = debug \ No newline at end of file diff --git a/firmware/src/audio.c b/firmware/src/audio.c index a8a7c69..aa08818 100644 --- a/firmware/src/audio.c +++ b/firmware/src/audio.c @@ -13,6 +13,12 @@ #define AUDIO_THREAD_STACK_SIZE 2048 #define AUDIO_THREAD_PRIORITY 5 +#define AUDIO_PATH "/lfs/a" +#define AUDIO_BLOCK_SIZE 8192 /* 512 Samples Stereo (16-bit) = 8192 Bytes */ +#define AUDIO_BLOCK_COUNT 4 +#define AUDIO_WORD_WIDTH 16 +#define AUDIO_SAMPLE_RATE 16000 + LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG); /* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */ @@ -48,25 +54,39 @@ static char next_random_filename[64] = {0}; static uint32_t audio_file_count = 0; static char cached_404_path[] = "/lfs/sys/404"; +static struct k_mutex i2s_lock; +static struct k_work audio_stop_work; + +static void audio_stop_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + + k_mutex_lock(&i2s_lock, K_FOREVER); + + enum pm_device_state state; + pm_device_state_get(i2s_dev, &state); + + if (state == PM_DEVICE_STATE_ACTIVE) + { + LOG_DBG("Triggering I2S DROP to stop ongoing transmission"); + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); + } + + k_mutex_unlock(&i2s_lock); +} + void i2s_suspend(void) { - LOG_DBG("Suspending I2S interface for power saving"); - i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); - - /* Nutzt jetzt die korrekte Spezifikation */ - gpio_pin_set_dt(&_en_dev, 0); - + k_mutex_lock(&i2s_lock, K_FOREVER); // Sperren pm_device_action_run(i2s_dev, PM_DEVICE_ACTION_SUSPEND); + k_mutex_unlock(&i2s_lock); // Freigeben } void i2s_resume(void) { - LOG_DBG("Resuming I2S interface"); - - /* Zuerst Pin auf High, dann Hardware wecken */ - gpio_pin_set_dt(&_en_dev, 1); - + k_mutex_lock(&i2s_lock, K_FOREVER); pm_device_action_run(i2s_dev, PM_DEVICE_ACTION_RESUME); + k_mutex_unlock(&i2s_lock); } void audio_refresh_file_count(void) @@ -154,7 +174,13 @@ void audio_stop(void) LOG_DBG("Playback abort requested"); abort_playback = true; k_msgq_purge(&audio_play_msgq); - i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); + + if (k_is_in_isr()) + { + LOG_DBG("audio_stop called from ISR, deferring I2S DROP"); + } + + k_work_submit(&audio_stop_work); } void audio_play(const char *filename) @@ -372,6 +398,8 @@ int audio_init(void) } gpio_pin_configure_dt(&_en_dev, 0); + k_mutex_init(&i2s_lock); + k_work_init(&audio_stop_work, audio_stop_work_handler); audio_refresh_file_count(); LOG_INF("Audio initialized: %u bits, %u.%03u kHz", config.word_size, config.frame_clk_freq / 1000, config.frame_clk_freq % 1000); diff --git a/firmware/src/audio.h b/firmware/src/audio.h index 2256949..18dab87 100644 --- a/firmware/src/audio.h +++ b/firmware/src/audio.h @@ -1,18 +1,6 @@ #ifndef AUDIO_H #define AUDIO_H -#define AUDIO_PATH "/lfs/a" -#define AUDIO_EVENT_PLAY BIT(0) -#define AUDIO_EVENT_STOP BIT(1) -#define AUDIO_EVENT_SYNC BIT(8) - -#define AUDIO_EVENTS_MASK (AUDIO_EVENT_PLAY | AUDIO_EVENT_STOP | AUDIO_EVENT_SYNC) - -#define AUDIO_BLOCK_SIZE 8192 /* 512 Samples Stereo (16-bit) = 8192 Bytes */ -#define AUDIO_BLOCK_COUNT 4 -#define AUDIO_WORD_WIDTH 16 -#define AUDIO_SAMPLE_RATE 16000 - /** * @brief Initializes the audio subsystem * diff --git a/firmware/src/io.c b/firmware/src/io.c index 18d9c12..cb9ba25 100644 --- a/firmware/src/io.c +++ b/firmware/src/io.c @@ -14,14 +14,25 @@ static const struct gpio_dt_spec usb_led_spec = GPIO_DT_SPEC_GET(USB_LED_NODE, g static const struct gpio_dt_spec button_spec = GPIO_DT_SPEC_GET(BUZZER_BUTTON_NODE, gpios); static struct gpio_callback button_cb_data; +static struct k_work button_audio_work; static struct k_work_delayable debounce_work; -void button_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { - gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_DISABLE); - +static void button_audio_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); LOG_DBG("Button pressed, triggering audio play"); audio_stop(); audio_play(NULL); +} + +void button_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { + ARG_UNUSED(dev); + ARG_UNUSED(cb); + ARG_UNUSED(pins); + + gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_DISABLE); + + k_work_submit(&button_audio_work); k_work_reschedule(&debounce_work, K_MSEC(50)); } @@ -71,6 +82,7 @@ int io_init(void) return ret; } + k_work_init(&button_audio_work, button_audio_work_handler); k_work_init_delayable(&debounce_work, debounce_work_handler); gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_EDGE_TO_ACTIVE); gpio_init_callback(&button_cb_data, button_isr, BIT(button_spec.pin)); diff --git a/webpage/src/components/ConnectButton.svelte b/webpage/src/components/ConnectButton.svelte index a4e364e..1c266f0 100644 --- a/webpage/src/components/ConnectButton.svelte +++ b/webpage/src/components/ConnectButton.svelte @@ -1,58 +1,37 @@ \ No newline at end of file diff --git a/webpage/src/components/ToastContainer.svelte b/webpage/src/components/ToastContainer.svelte new file mode 100644 index 0000000..e69de29 diff --git a/webpage/src/lib/buzzerActions.ts b/webpage/src/lib/buzzerActions.ts index 4fa0958..c68d52b 100644 --- a/webpage/src/lib/buzzerActions.ts +++ b/webpage/src/lib/buzzerActions.ts @@ -1,5 +1,8 @@ import { buzzer } from './buzzerStore'; import { get } from 'svelte/store'; +import { addToast } from './toastStore'; + +let isConnecting = false; type Task = { command: string; @@ -51,12 +54,22 @@ class SerialQueue { const { value, done } = await reader.read(); if (done) break; raw += decoder.decode(value); - if (raw.includes("OK")) break; +if (raw.includes("OK") || raw.includes("ERR")) break; // Auf ERR achten! } + reader.releaseLock(); - task.resolve(raw.split('\n').map(l => l.trim()).filter(l => l && l !== "OK" && !l.startsWith(task.command))); + const lines = raw.split('\n').map(l => l.trim()).filter(l => l); + + // AUTOMATISCHE FEHLERERKENNUNG + const errorLine = lines.find(l => l.startsWith("ERR")); + if (errorLine) { + addToast(`Gerätefehler: ${errorLine}`, 'error', 5000); + } + + task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command))); } catch (e) { - console.error("Queue Error:", e); + addToast("Kommunikationsfehler (Serial Port)", "error"); + console.error(e); } finally { this.isProcessing = false; this.process(); @@ -66,6 +79,87 @@ class SerialQueue { const queue = new SerialQueue(); +/** + * Initialisiert die globalen Serial-Listener (aufgerufen beim Start der App) + */ +export function initSerialListeners() { + if (typeof navigator === 'undefined' || !navigator.serial) return; + + // 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird + navigator.serial.addEventListener('connect', (event) => { + console.log('Neues Gerät erkannt, starte Auto-Connect...'); + autoConnect(); + }); + + // Beim Laden der Seite prüfen, ob wir bereits Zugriff auf Geräte haben + autoConnect(); +} + +/** + * Versucht eine Verbindung zu bereits gekoppelten Geräten herzustellen + */ +export async function autoConnect() { + if (typeof navigator === 'undefined' || !navigator.serial) return; + + const ports = await navigator.serial.getPorts(); + if (ports.length > 0) { + // Wir nehmen den ersten verfügbaren Port + const port = ports[0]; + console.log('Gekoppeltes Gerät gefunden, verbinde...'); + + try { + await connectToPort(port); + } catch (e) { + console.warn('Auto-Connect fehlgeschlagen:', e); + } + } +} + +/** + * Kernfunktion für den Verbindungsaufbau + */ +export async function connectToPort(port: SerialPort) { + if (isConnecting || get(buzzer).connected) return; + isConnecting = true; + + try { + await port.open({ baudRate: 115200 }); + + port.addEventListener('disconnect', () => { + addToast("Buzzer-Verbindung verloren!", "warning"); + handleDisconnect(); + }); + + setActivePort(port); + buzzer.update(s => ({ ...s, connected: true })); + + // Kleiner Timeout, damit die UI Zeit zum Hydrieren hat + setTimeout(() => { + addToast("Buzzer erfolgreich verbunden", "success"); + }, 100); + + await updateDeviceInfo(port); + await refreshFileList(); + } catch (e) { + console.error("Connect error:", e); + // Nur Toasten, wenn es kein "Already Open" Fehler ist + if (!(e instanceof Error && e.name === 'InvalidStateError')) { + addToast("Verbindung fehlgeschlagen", "error"); + } + } finally { + isConnecting = false; + } +} + +function handleDisconnect() { + setActivePort(null); + buzzer.update(s => ({ + ...s, + connected: false, + files: [] // Liste leeren, da Gerät weg + })); +} + // --- EXPORTE --- export function setActivePort(port: SerialPort | null) { diff --git a/webpage/src/lib/toastStore.ts b/webpage/src/lib/toastStore.ts new file mode 100644 index 0000000..a2eda1f --- /dev/null +++ b/webpage/src/lib/toastStore.ts @@ -0,0 +1,27 @@ +import { writable } from 'svelte/store'; + +export type ToastType = 'success' | 'error' | 'info' | 'warning'; + +export interface Toast { + id: number; + message: string; + type: ToastType; + duration?: number; +} + +export const toasts = writable([]); + +export function addToast(message: string, type: ToastType = 'info', duration = 3000) { + const id = Date.now(); + toasts.update(all => [{ id, message, type, duration }, ...all]); + + if (duration > 0) { + setTimeout(() => { + removeToast(id); + }, duration); + } +} + +export function removeToast(id: number) { + toasts.update(all => all.filter(t => t.id !== id)); +} \ No newline at end of file diff --git a/webpage/src/pages/index.astro b/webpage/src/pages/index.astro index d8b4645..0164e11 100644 --- a/webpage/src/pages/index.astro +++ b/webpage/src/pages/index.astro @@ -8,6 +8,8 @@ import ConnectButton from "../components/ConnectButton.svelte"; import SerialWarning from "../components/SerialWarning.svelte"; import FileStorage from "../components/FileStorage.svelte"; import DeviceInfo from "../components/DeviceInfo.svelte"; +import ToastContainer from "../components/ToastContainer.svelte"; +import type { loadRenderers } from "astro:container"; --- @@ -18,7 +20,7 @@ import DeviceInfo from "../components/DeviceInfo.svelte"; - +