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";
-
+