sync
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 1
|
||||
PATCHLEVEL = 12
|
||||
PATCHLEVEL = 14
|
||||
VERSION_TWEAK = 0
|
||||
EXTRAVERSION = 0
|
||||
EXTRAVERSION = debug
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,58 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { buzzer } from '../lib/buzzerStore';
|
||||
import { updateDeviceInfo, refreshFileList, setActivePort } from '../lib/buzzerActions';
|
||||
import { set } from 'astro:schema';
|
||||
import { connectToPort, initSerialListeners } from '../lib/buzzerActions';
|
||||
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
|
||||
|
||||
let port: SerialPort | null = null;
|
||||
const filters = [{ usbVendorId: 0x2fe3, usbProductId: 0x0001 }];
|
||||
onMount(() => {
|
||||
initSerialListeners();
|
||||
});
|
||||
|
||||
async function connect() {
|
||||
async function handleConnectClick() {
|
||||
try {
|
||||
port = await navigator.serial.requestPort({ filters });
|
||||
await port.open({ baudRate: 115200 });
|
||||
|
||||
// Store aktualisieren
|
||||
buzzer.update(b => ({ ...b, connected: true }));
|
||||
|
||||
if (port) {
|
||||
setActivePort(port); // Aktiven Port in den Actions setzen
|
||||
await updateDeviceInfo(port);
|
||||
await refreshFileList(port);
|
||||
}
|
||||
// Wenn wir schon gekoppelt sind, aber nicht verbunden,
|
||||
// wird navigator.serial.requestPort() den Picker zeigen.
|
||||
const port = await navigator.serial.requestPort();
|
||||
await connectToPort(port);
|
||||
} catch (e) {
|
||||
setActivePort(null); // Port zurücksetzen
|
||||
console.error("Verbindung fehlgeschlagen:", e);
|
||||
buzzer.update(b => ({ ...b, connected: false }));
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (port) {
|
||||
await port.close();
|
||||
port = null;
|
||||
buzzer.update(b => ({ ...b, connected: false }));
|
||||
console.error("Verbindung abgebrochen", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={$buzzer.connected ? disconnect : connect}
|
||||
class="flex items-center gap-2 px-6 py-2 {$buzzer.connected ? 'bg-blue-600 hover:bg-blue-500' : 'bg-emerald-600 hover:bg-emerald-500'} text-white text-xs font-black rounded-full transition-all shadow-lg active:scale-95 uppercase tracking-widest"
|
||||
on:click={handleConnectClick}
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all
|
||||
{$buzzer.connected
|
||||
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/50'
|
||||
: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600'}"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256">
|
||||
{#if $buzzer.connected}
|
||||
<rect width="256" height="256" fill="none"/>
|
||||
<rect x="63.03" y="88.4" width="129.94" height="79.2" rx="24" transform="translate(-53.02 128) rotate(-45)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<line x1="88" y1="88" x2="168" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<line x1="232" y1="24" x2="173.94" y2="82.06" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<line x1="82.06" y1="173.94" x2="24" y2="232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<PlugsConnectedIcon size={18} weight="fill" />
|
||||
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Verbunden</span>
|
||||
{:else}
|
||||
<rect width="256" height="256" fill="none"/>
|
||||
<line x1="144" y1="144" x2="120" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<path d="M132,180l-29,29a24,24,0,0,1-33.94,0L47,186.91A24,24,0,0,1,47,153l29-29" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<line x1="197.94" y1="58.06" x2="232" y2="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<path d="M180,132l29-29a24,24,0,0,0,0-33.94L186.91,47A24,24,0,0,0,153,47L124,76" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
|
||||
<PlugsIcon size={18} weight="bold" />
|
||||
<span class="text-xs font-bold uppercase tracking-wider">Verbinden</span>
|
||||
{/if}
|
||||
</svg>
|
||||
{$buzzer.connected ? 'Disconnect' : 'Connect'}
|
||||
</button>
|
||||
0
webpage/src/components/ToastContainer.svelte
Normal file
0
webpage/src/components/ToastContainer.svelte
Normal file
@@ -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) {
|
||||
|
||||
27
webpage/src/lib/toastStore.ts
Normal file
27
webpage/src/lib/toastStore.ts
Normal file
@@ -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<Toast[]>([]);
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -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";
|
||||
---
|
||||
|
||||
<html lang="de">
|
||||
@@ -18,7 +20,7 @@ import DeviceInfo from "../components/DeviceInfo.svelte";
|
||||
<body class="bg-slate-900 selection:bg-blue-500/30">
|
||||
<SerialWarning client:load />
|
||||
<StatusModal visible={false} />
|
||||
|
||||
<ToastContainer client:load />
|
||||
<div class="min-h-screen max-w-[1600px] min-w-[1024px] mx-auto text-slate-100 flex flex-col font-sans">
|
||||
<header
|
||||
class="h-16 border-b border-slate-700 bg-slate-800 grid grid-cols-3 items-center px-8 shrink-0 shadow-lg">
|
||||
|
||||
Reference in New Issue
Block a user