first commit
This commit is contained in:
commit
0b09765013
|
|
@ -0,0 +1,11 @@
|
|||
# ESP32-S3 Bluetooth SIP Client
|
||||
# SIP-Telefon mit Bluetooth und USB-Headset Unterstützung
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Setze IDF_TARGET auf ESP32-S3 für USB OTG Support
|
||||
set(IDF_TARGET esp32s3)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
project(bluetooth-sip-client)
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
# ESP32-S3 Bluetooth SIP Client
|
||||
|
||||
Ein SIP-Telefon basierend auf ESP32-S3 mit Bluetooth- und USB-Headset-Unterstützung. Entwickelt für Thin-Client-Umgebungen ohne nativen CTI-Support.
|
||||
|
||||
## Features
|
||||
|
||||
- **WiFi-Konfiguration**
|
||||
- Automatischer Hotspot-Modus wenn keine WLAN-Daten konfiguriert
|
||||
- Web-basierte Konfiguration (SSID: `ESP32-SIP-Phone`, IP: `192.168.4.1`)
|
||||
- DHCP oder statische IP-Konfiguration
|
||||
- Automatischer Fallback zu Hotspot bei Verbindungsproblemen
|
||||
|
||||
- **Bluetooth Headsets**
|
||||
- HFP (Hands-Free Profile) Unterstützung
|
||||
- Mehrere Geräte pairen (>5 möglich)
|
||||
- Auto-Connect für bekannte Geräte
|
||||
- Headset-Tasten: Annehmen, Auflegen, Lautstärke
|
||||
|
||||
- **USB Headsets**
|
||||
- USB Audio Class Unterstützung
|
||||
- USB-C direkter Anschluss oder USB-A mit Adapter
|
||||
- **USB hat immer Priorität über Bluetooth**
|
||||
- Automatische Erkennung
|
||||
|
||||
- **SIP/VoIP**
|
||||
- SIP-Registrierung an TK-Anlage
|
||||
- Eingehende und ausgehende Anrufe
|
||||
- G.711 µ-law und A-law Codec
|
||||
- Digest Authentication
|
||||
|
||||
- **Weboberfläche**
|
||||
- Status-Übersicht
|
||||
- WLAN-Konfiguration mit Scan
|
||||
- SIP-Einstellungen
|
||||
- Bluetooth-Geräte-Management
|
||||
- System-Einstellungen (Neustart, Werksreset)
|
||||
|
||||
## Hardware
|
||||
|
||||
### Empfohlenes Board
|
||||
- **ESP32-S3-DevKitC-1** (oder ähnlich mit USB OTG)
|
||||
- Mindestens 4MB Flash
|
||||
- Optional: PSRAM für bessere Audio-Pufferung
|
||||
|
||||
### Anschlüsse
|
||||
- **USB-C (USB OTG)**: Für USB-Headsets
|
||||
- **WLAN**: 2.4GHz WiFi
|
||||
|
||||
## Installation
|
||||
|
||||
### Voraussetzungen
|
||||
- ESP-IDF v5.0 oder neuer
|
||||
- USB-Kabel für Programmierung
|
||||
|
||||
### Build & Flash
|
||||
```bash
|
||||
# ESP-IDF Umgebung aktivieren
|
||||
. $IDF_PATH/export.sh
|
||||
|
||||
# Konfigurieren
|
||||
idf.py set-target esp32s3
|
||||
idf.py menuconfig
|
||||
|
||||
# Bauen und Flashen
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
## Ersteinrichtung
|
||||
|
||||
1. **ESP32 mit Strom versorgen**
|
||||
- LED sollte nach Boot leuchten
|
||||
|
||||
2. **Mit Hotspot verbinden**
|
||||
- SSID: `ESP32-SIP-Phone`
|
||||
- Passwort: `sip-phone-setup`
|
||||
|
||||
3. **Weboberfläche öffnen**
|
||||
- Browser: `http://192.168.4.1`
|
||||
|
||||
4. **WLAN konfigurieren**
|
||||
- Tab "WLAN" → Netzwerk auswählen → Verbinden
|
||||
|
||||
5. **SIP konfigurieren**
|
||||
- Tab "SIP" → Server, Benutzername, Passwort eingeben
|
||||
|
||||
6. **Bluetooth Headset verbinden**
|
||||
- Headset in Pairing-Modus setzen
|
||||
- Tab "Bluetooth" → Gerät pairen
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Methode | Beschreibung |
|
||||
|----------|---------|--------------|
|
||||
| `/api/status` | GET | Gesamtstatus |
|
||||
| `/api/wifi/config` | GET/POST | WLAN-Konfiguration |
|
||||
| `/api/wifi/scan` | GET | WLAN scannen |
|
||||
| `/api/sip/config` | GET/POST | SIP-Konfiguration |
|
||||
| `/api/bluetooth/devices` | GET | Gepaarte BT-Geräte |
|
||||
| `/api/bluetooth/scan` | POST | BT-Scan starten |
|
||||
| `/api/bluetooth/pair` | POST | Gerät pairen |
|
||||
| `/api/call/answer` | POST | Anruf annehmen |
|
||||
| `/api/call/hangup` | POST | Anruf beenden |
|
||||
| `/api/system/reboot` | POST | Neustart |
|
||||
| `/api/system/factory-reset` | POST | Werksreset |
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
bluetooth-sip-client/
|
||||
├── CMakeLists.txt
|
||||
├── sdkconfig.defaults
|
||||
├── partitions.csv
|
||||
└── main/
|
||||
├── main.c # Hauptprogramm
|
||||
├── config/
|
||||
│ ├── config_manager.h # Konfiguration Header
|
||||
│ └── config_manager.c # NVS-basierte Config
|
||||
├── wifi/
|
||||
│ ├── wifi_manager.h # WiFi Header
|
||||
│ └── wifi_manager.c # WiFi AP/STA Manager
|
||||
├── web/
|
||||
│ ├── web_server.h # Webserver Header
|
||||
│ ├── web_server.c # HTTP Server
|
||||
│ ├── web_api.h # API Header
|
||||
│ ├── web_api.c # REST API
|
||||
│ └── static/ # Web UI
|
||||
│ ├── index.html
|
||||
│ ├── style.css
|
||||
│ └── app.js
|
||||
├── bluetooth/
|
||||
│ ├── bt_manager.h # BT Manager Header
|
||||
│ ├── bt_manager.c # BT Geräteverwaltung
|
||||
│ ├── bt_hfp.h # HFP Header
|
||||
│ └── bt_hfp.c # HFP Audio Gateway
|
||||
├── usb_audio/
|
||||
│ ├── usb_audio_host.h # USB Audio Header
|
||||
│ └── usb_audio_host.c # USB Audio Class Host
|
||||
├── audio/
|
||||
│ ├── audio_router.h # Audio Router Header
|
||||
│ └── audio_router.c # Audio Routing & Priorität
|
||||
└── sip/
|
||||
├── sip_client.h # SIP Client Header
|
||||
├── sip_client.c # SIP UA Implementation
|
||||
├── sip_parser.h # SIP Parser Header
|
||||
└── sip_parser.c # SIP Message Parser
|
||||
```
|
||||
|
||||
## Einschränkungen
|
||||
|
||||
- **Bluetooth**: Maximal 3-4 gleichzeitig aktive Verbindungen (ESP32 Limitation), aber mehr Geräte können gepaart werden
|
||||
- **USB Audio**: Grundlegende UAC1 Unterstützung, nicht alle Headsets getestet
|
||||
- **Codecs**: Aktuell nur G.711 (PCMU/PCMA)
|
||||
- **SRTP**: Nicht unterstützt (nur unverschlüsseltes RTP)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Kein WLAN nach Konfiguration
|
||||
- Gerät neu starten
|
||||
- Hotspot sollte nach 30 Sekunden automatisch starten
|
||||
|
||||
### Bluetooth Headset verbindet nicht
|
||||
- Headset zurücksetzen und erneut pairen
|
||||
- Andere Bluetooth-Verbindungen am Headset trennen
|
||||
|
||||
### Kein Audio bei Anruf
|
||||
- USB-Headset hat Priorität - bei Verwendung von BT das USB-Kabel entfernen
|
||||
- Audio-Routing prüfen im Status-Tab
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT License
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"config/config_manager.c"
|
||||
"wifi/wifi_manager.c"
|
||||
"web/web_server.c"
|
||||
"web/web_api.c"
|
||||
"bluetooth/bt_manager.c"
|
||||
"bluetooth/bt_hfp.c"
|
||||
"usb_audio/usb_audio_host.c"
|
||||
"audio/audio_router.c"
|
||||
"sip/sip_client.c"
|
||||
"sip/sip_parser.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"config"
|
||||
"wifi"
|
||||
"web"
|
||||
"bluetooth"
|
||||
"usb_audio"
|
||||
"audio"
|
||||
"sip"
|
||||
EMBED_FILES
|
||||
"web/static/index.html"
|
||||
"web/static/style.css"
|
||||
"web/static/app.js"
|
||||
REQUIRES
|
||||
nvs_flash
|
||||
esp_wifi
|
||||
esp_netif
|
||||
esp_http_server
|
||||
bt
|
||||
usb
|
||||
driver
|
||||
esp_event
|
||||
json
|
||||
lwip
|
||||
mdns
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
menu "Bluetooth SIP Client Configuration"
|
||||
|
||||
config BSC_DEFAULT_AP_SSID
|
||||
string "Default Hotspot SSID"
|
||||
default "ESP32-SIP-Phone"
|
||||
help
|
||||
SSID für den Standard-Hotspot wenn keine WLAN-Daten konfiguriert sind.
|
||||
|
||||
config BSC_DEFAULT_AP_PASSWORD
|
||||
string "Default Hotspot Password"
|
||||
default "sip-phone-setup"
|
||||
help
|
||||
Passwort für den Standard-Hotspot.
|
||||
|
||||
config BSC_DEFAULT_AP_IP
|
||||
string "Default Hotspot IP"
|
||||
default "192.168.4.1"
|
||||
help
|
||||
IP-Adresse des ESP32 im Hotspot-Modus.
|
||||
|
||||
config BSC_DEFAULT_AP_GATEWAY
|
||||
string "Default Hotspot Gateway"
|
||||
default "192.168.4.1"
|
||||
help
|
||||
Gateway-Adresse im Hotspot-Modus.
|
||||
|
||||
config BSC_DEFAULT_AP_NETMASK
|
||||
string "Default Hotspot Netmask"
|
||||
default "255.255.255.0"
|
||||
help
|
||||
Netzmaske im Hotspot-Modus.
|
||||
|
||||
config BSC_WEB_PORT
|
||||
int "Webserver Port"
|
||||
default 80
|
||||
help
|
||||
Port für den Konfigurations-Webserver.
|
||||
|
||||
config BSC_MAX_BT_DEVICES
|
||||
int "Maximum Bluetooth Paired Devices"
|
||||
default 10
|
||||
help
|
||||
Maximale Anzahl gepairter Bluetooth-Geräte.
|
||||
|
||||
config BSC_BT_DEVICE_NAME
|
||||
string "Bluetooth Device Name"
|
||||
default "ESP32-SIP-Phone"
|
||||
help
|
||||
Bluetooth-Gerätename für die Erkennung.
|
||||
|
||||
config BSC_SIP_DEFAULT_PORT
|
||||
int "Default SIP Port"
|
||||
default 5060
|
||||
help
|
||||
Standard SIP-Port für die TK-Anlage.
|
||||
|
||||
endmenu
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
/**
|
||||
* Audio Router - Verwaltet Audio-Quellen und -Routing
|
||||
*
|
||||
* USB hat Priorität über Bluetooth
|
||||
* Automatischer Wechsel bei Verbindungsänderungen
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "audio_router.h"
|
||||
#include "bluetooth/bt_manager.h"
|
||||
#include "usb_audio/usb_audio_host.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
static const char* TAG = "AUDIO_RT";
|
||||
|
||||
// State
|
||||
static bool s_initialized = false;
|
||||
static audio_source_t s_active_source = AUDIO_SOURCE_NONE;
|
||||
static bool s_call_active = false;
|
||||
static uint8_t s_volume = 80;
|
||||
static bool s_muted = false;
|
||||
static audio_stats_t s_stats;
|
||||
static SemaphoreHandle_t s_mutex = NULL;
|
||||
|
||||
// Callbacks
|
||||
static audio_from_headset_callback_t s_input_callback = NULL;
|
||||
static headset_button_callback_t s_button_callback = NULL;
|
||||
static audio_source_change_callback_t s_source_change_callback = NULL;
|
||||
|
||||
// Forward Declarations
|
||||
static void update_active_source(void);
|
||||
static void bt_audio_data_handler(const uint8_t* data, size_t len);
|
||||
static void bt_button_handler(bt_button_event_t event);
|
||||
static void usb_audio_data_handler(const uint8_t* data, size_t len);
|
||||
static void usb_button_handler(usb_button_event_t event);
|
||||
static void usb_state_handler(usb_audio_state_t state);
|
||||
|
||||
// Callback von Bluetooth Manager
|
||||
static void bt_audio_data_handler(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (s_active_source != AUDIO_SOURCE_BLUETOOTH || !s_call_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_stats.packets_received++;
|
||||
|
||||
// An SIP weiterleiten
|
||||
if (s_input_callback) {
|
||||
audio_format_t format = {
|
||||
.sample_rate = 8000, // HFP Standard
|
||||
.channels = 1,
|
||||
.bits_per_sample = 16
|
||||
};
|
||||
s_input_callback(data, len, &format);
|
||||
}
|
||||
}
|
||||
|
||||
static void bt_button_handler(bt_button_event_t event)
|
||||
{
|
||||
headset_button_t button;
|
||||
|
||||
switch (event) {
|
||||
case BT_BUTTON_ANSWER:
|
||||
button = HEADSET_BUTTON_ANSWER;
|
||||
break;
|
||||
case BT_BUTTON_REJECT:
|
||||
button = HEADSET_BUTTON_REJECT;
|
||||
break;
|
||||
case BT_BUTTON_HANGUP:
|
||||
button = HEADSET_BUTTON_HANGUP;
|
||||
break;
|
||||
case BT_BUTTON_VOLUME_UP:
|
||||
button = HEADSET_BUTTON_VOLUME_UP;
|
||||
break;
|
||||
case BT_BUTTON_VOLUME_DOWN:
|
||||
button = HEADSET_BUTTON_VOLUME_DOWN;
|
||||
break;
|
||||
case BT_BUTTON_MUTE:
|
||||
button = HEADSET_BUTTON_MUTE;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_button_callback) {
|
||||
s_button_callback(button, AUDIO_SOURCE_BLUETOOTH);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback von USB Audio
|
||||
static void usb_audio_data_handler(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (s_active_source != AUDIO_SOURCE_USB || !s_call_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_stats.packets_received++;
|
||||
|
||||
// An SIP weiterleiten
|
||||
if (s_input_callback) {
|
||||
audio_format_t format = {
|
||||
.sample_rate = 16000, // USB typisch
|
||||
.channels = 1,
|
||||
.bits_per_sample = 16
|
||||
};
|
||||
s_input_callback(data, len, &format);
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_button_handler(usb_button_event_t event)
|
||||
{
|
||||
headset_button_t button;
|
||||
|
||||
switch (event) {
|
||||
case USB_BUTTON_ANSWER:
|
||||
button = HEADSET_BUTTON_ANSWER;
|
||||
break;
|
||||
case USB_BUTTON_HANGUP:
|
||||
button = HEADSET_BUTTON_HANGUP;
|
||||
break;
|
||||
case USB_BUTTON_MUTE:
|
||||
button = HEADSET_BUTTON_MUTE;
|
||||
break;
|
||||
case USB_BUTTON_VOLUME_UP:
|
||||
button = HEADSET_BUTTON_VOLUME_UP;
|
||||
break;
|
||||
case USB_BUTTON_VOLUME_DOWN:
|
||||
button = HEADSET_BUTTON_VOLUME_DOWN;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_button_callback) {
|
||||
s_button_callback(button, AUDIO_SOURCE_USB);
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_state_handler(usb_audio_state_t state)
|
||||
{
|
||||
ESP_LOGI(TAG, "USB Audio State: %d", state);
|
||||
|
||||
// Bei USB Verbindungsänderung Quelle neu evaluieren
|
||||
update_active_source();
|
||||
}
|
||||
|
||||
static void update_active_source(void)
|
||||
{
|
||||
audio_source_t new_source = AUDIO_SOURCE_NONE;
|
||||
|
||||
xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||
|
||||
// USB hat höchste Priorität
|
||||
if (usb_audio_host_is_connected()) {
|
||||
new_source = AUDIO_SOURCE_USB;
|
||||
}
|
||||
// Bluetooth als Fallback
|
||||
else if (bt_manager_is_connected()) {
|
||||
new_source = AUDIO_SOURCE_BLUETOOTH;
|
||||
}
|
||||
|
||||
if (new_source != s_active_source) {
|
||||
audio_source_t old_source = s_active_source;
|
||||
s_active_source = new_source;
|
||||
|
||||
const char* sources[] = {"Keine", "USB", "Bluetooth"};
|
||||
ESP_LOGI(TAG, "Audio-Quelle: %s -> %s", sources[old_source], sources[new_source]);
|
||||
|
||||
// Bei aktivem Anruf: Streaming anpassen
|
||||
if (s_call_active) {
|
||||
// Altes Audio stoppen
|
||||
if (old_source == AUDIO_SOURCE_USB) {
|
||||
usb_audio_host_stop_stream();
|
||||
}
|
||||
// Neues Audio starten
|
||||
if (new_source == AUDIO_SOURCE_USB) {
|
||||
usb_audio_host_start_stream();
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_mutex);
|
||||
|
||||
// Callback aufrufen
|
||||
if (s_source_change_callback) {
|
||||
s_source_change_callback(old_source, new_source);
|
||||
}
|
||||
} else {
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t audio_router_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initialisiere Audio Router");
|
||||
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
if (!s_mutex) {
|
||||
ESP_LOGE(TAG, "Mutex erstellen fehlgeschlagen");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// Callbacks bei Audio-Quellen registrieren
|
||||
bt_manager_register_audio_data_callback(bt_audio_data_handler);
|
||||
bt_manager_register_button_callback(bt_button_handler);
|
||||
|
||||
usb_audio_host_register_data_callback(usb_audio_data_handler);
|
||||
usb_audio_host_register_button_callback(usb_button_handler);
|
||||
usb_audio_host_register_state_callback(usb_state_handler);
|
||||
|
||||
memset(&s_stats, 0, sizeof(s_stats));
|
||||
|
||||
s_initialized = true;
|
||||
|
||||
// Initial die Quelle bestimmen
|
||||
update_active_source();
|
||||
|
||||
ESP_LOGI(TAG, "Audio Router initialisiert");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t audio_router_deinit(void)
|
||||
{
|
||||
if (!s_initialized) return ESP_OK;
|
||||
|
||||
ESP_LOGI(TAG, "Deinitalisiere Audio Router");
|
||||
|
||||
if (s_call_active) {
|
||||
audio_router_stop_call();
|
||||
}
|
||||
|
||||
if (s_mutex) {
|
||||
vSemaphoreDelete(s_mutex);
|
||||
s_mutex = NULL;
|
||||
}
|
||||
|
||||
s_initialized = false;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
audio_source_t audio_router_get_active_source(void)
|
||||
{
|
||||
return s_active_source;
|
||||
}
|
||||
|
||||
bool audio_router_is_source_available(audio_source_t source)
|
||||
{
|
||||
switch (source) {
|
||||
case AUDIO_SOURCE_USB:
|
||||
return usb_audio_host_is_connected();
|
||||
case AUDIO_SOURCE_BLUETOOTH:
|
||||
return bt_manager_is_connected();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t audio_router_start_call(void)
|
||||
{
|
||||
if (s_call_active) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starte Audio für Anruf");
|
||||
|
||||
xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||
|
||||
s_call_active = true;
|
||||
memset(&s_stats, 0, sizeof(s_stats));
|
||||
|
||||
// Audio-Streaming auf aktiver Quelle starten
|
||||
if (s_active_source == AUDIO_SOURCE_USB) {
|
||||
usb_audio_host_start_stream();
|
||||
} else if (s_active_source == AUDIO_SOURCE_BLUETOOTH) {
|
||||
// BT Audio wird automatisch über HFP gestartet
|
||||
// bt_hfp_audio_connect() wird vom SIP-Client aufgerufen
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_mutex);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t audio_router_stop_call(void)
|
||||
{
|
||||
if (!s_call_active) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stoppe Audio für Anruf");
|
||||
|
||||
xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||
|
||||
s_call_active = false;
|
||||
|
||||
// Audio-Streaming stoppen
|
||||
if (s_active_source == AUDIO_SOURCE_USB) {
|
||||
usb_audio_host_stop_stream();
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_mutex);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t audio_router_send_to_headset(const uint8_t* data, size_t len, const audio_format_t* format)
|
||||
{
|
||||
if (!s_call_active || s_active_source == AUDIO_SOURCE_NONE) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (s_muted) {
|
||||
return ESP_OK; // Muted - nichts senden
|
||||
}
|
||||
|
||||
s_stats.packets_sent++;
|
||||
|
||||
// TODO: Lautstärke anwenden
|
||||
// TODO: Format-Konvertierung wenn nötig
|
||||
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
if (s_active_source == AUDIO_SOURCE_USB) {
|
||||
ret = usb_audio_host_send(data, len);
|
||||
} else if (s_active_source == AUDIO_SOURCE_BLUETOOTH) {
|
||||
ret = bt_manager_send_audio(data, len);
|
||||
}
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
s_stats.underruns++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void audio_router_register_input_callback(audio_from_headset_callback_t callback)
|
||||
{
|
||||
s_input_callback = callback;
|
||||
}
|
||||
|
||||
void audio_router_register_button_callback(headset_button_callback_t callback)
|
||||
{
|
||||
s_button_callback = callback;
|
||||
}
|
||||
|
||||
void audio_router_register_source_change_callback(audio_source_change_callback_t callback)
|
||||
{
|
||||
s_source_change_callback = callback;
|
||||
}
|
||||
|
||||
esp_err_t audio_router_set_volume(uint8_t volume)
|
||||
{
|
||||
if (volume > 100) volume = 100;
|
||||
s_volume = volume;
|
||||
|
||||
ESP_LOGI(TAG, "Lautstärke: %d%%", volume);
|
||||
|
||||
// Lautstärke an aktive Quelle weitergeben
|
||||
if (s_active_source == AUDIO_SOURCE_USB) {
|
||||
usb_audio_host_set_volume(volume);
|
||||
}
|
||||
// BT: Volume wird über HFP AG gehandhabt
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t audio_router_get_volume(void)
|
||||
{
|
||||
return s_volume;
|
||||
}
|
||||
|
||||
esp_err_t audio_router_set_mute(bool mute)
|
||||
{
|
||||
s_muted = mute;
|
||||
|
||||
ESP_LOGI(TAG, "Mute: %s", mute ? "an" : "aus");
|
||||
|
||||
if (s_active_source == AUDIO_SOURCE_USB) {
|
||||
usb_audio_host_set_mute(mute);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool audio_router_is_muted(void)
|
||||
{
|
||||
return s_muted;
|
||||
}
|
||||
|
||||
esp_err_t audio_router_get_stats(audio_stats_t* stats)
|
||||
{
|
||||
if (!stats) return ESP_ERR_INVALID_ARG;
|
||||
memcpy(stats, &s_stats, sizeof(audio_stats_t));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Audio-Quelle
|
||||
typedef enum {
|
||||
AUDIO_SOURCE_NONE = 0,
|
||||
AUDIO_SOURCE_USB, // USB Headset (höchste Priorität)
|
||||
AUDIO_SOURCE_BLUETOOTH // Bluetooth Headset
|
||||
} audio_source_t;
|
||||
|
||||
// Audio-Format
|
||||
typedef struct {
|
||||
uint32_t sample_rate;
|
||||
uint8_t channels;
|
||||
uint8_t bits_per_sample;
|
||||
} audio_format_t;
|
||||
|
||||
// Audio-Statistiken
|
||||
typedef struct {
|
||||
uint32_t packets_sent;
|
||||
uint32_t packets_received;
|
||||
uint32_t underruns;
|
||||
uint32_t overruns;
|
||||
int16_t input_level_db;
|
||||
int16_t output_level_db;
|
||||
} audio_stats_t;
|
||||
|
||||
// Button Event Callback (vereinheitlicht USB und BT)
|
||||
typedef enum {
|
||||
HEADSET_BUTTON_ANSWER = 0,
|
||||
HEADSET_BUTTON_HANGUP,
|
||||
HEADSET_BUTTON_REJECT,
|
||||
HEADSET_BUTTON_MUTE,
|
||||
HEADSET_BUTTON_VOLUME_UP,
|
||||
HEADSET_BUTTON_VOLUME_DOWN
|
||||
} headset_button_t;
|
||||
|
||||
typedef void (*headset_button_callback_t)(headset_button_t button, audio_source_t source);
|
||||
typedef void (*audio_source_change_callback_t)(audio_source_t old_source, audio_source_t new_source);
|
||||
|
||||
/**
|
||||
* Initialisiert den Audio-Router
|
||||
*/
|
||||
esp_err_t audio_router_init(void);
|
||||
|
||||
/**
|
||||
* Deinitalisiert den Audio-Router
|
||||
*/
|
||||
esp_err_t audio_router_deinit(void);
|
||||
|
||||
/**
|
||||
* Gibt die aktuell aktive Audio-Quelle zurück
|
||||
* USB hat immer Priorität über Bluetooth
|
||||
*/
|
||||
audio_source_t audio_router_get_active_source(void);
|
||||
|
||||
/**
|
||||
* Prüft ob eine Audio-Quelle verfügbar ist
|
||||
*/
|
||||
bool audio_router_is_source_available(audio_source_t source);
|
||||
|
||||
/**
|
||||
* Startet Audio-Routing für einen Anruf
|
||||
*/
|
||||
esp_err_t audio_router_start_call(void);
|
||||
|
||||
/**
|
||||
* Stoppt Audio-Routing
|
||||
*/
|
||||
esp_err_t audio_router_stop_call(void);
|
||||
|
||||
/**
|
||||
* Sendet Audio-Daten von SIP zum aktiven Headset
|
||||
*/
|
||||
esp_err_t audio_router_send_to_headset(const uint8_t* data, size_t len, const audio_format_t* format);
|
||||
|
||||
/**
|
||||
* Empfängt Audio-Daten vom aktiven Headset für SIP
|
||||
* Wird intern über Callback verarbeitet
|
||||
*/
|
||||
typedef void (*audio_from_headset_callback_t)(const uint8_t* data, size_t len, const audio_format_t* format);
|
||||
void audio_router_register_input_callback(audio_from_headset_callback_t callback);
|
||||
|
||||
/**
|
||||
* Registriert Callback für Headset-Button-Events
|
||||
*/
|
||||
void audio_router_register_button_callback(headset_button_callback_t callback);
|
||||
|
||||
/**
|
||||
* Registriert Callback für Quellenwechsel
|
||||
*/
|
||||
void audio_router_register_source_change_callback(audio_source_change_callback_t callback);
|
||||
|
||||
/**
|
||||
* Setzt die Master-Lautstärke (0-100)
|
||||
*/
|
||||
esp_err_t audio_router_set_volume(uint8_t volume);
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Lautstärke zurück
|
||||
*/
|
||||
uint8_t audio_router_get_volume(void);
|
||||
|
||||
/**
|
||||
* Setzt Mute
|
||||
*/
|
||||
esp_err_t audio_router_set_mute(bool mute);
|
||||
|
||||
/**
|
||||
* Gibt den Mute-Status zurück
|
||||
*/
|
||||
bool audio_router_is_muted(void);
|
||||
|
||||
/**
|
||||
* Gibt Audio-Statistiken zurück
|
||||
*/
|
||||
esp_err_t audio_router_get_stats(audio_stats_t* stats);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
/**
|
||||
* Bluetooth HFP - Hands-Free Profile Audio Gateway
|
||||
*
|
||||
* ESP32 agiert als Audio Gateway (AG) - die Rolle einer Telefonanlage
|
||||
* Headset ist das HF (Hands-Free) Device
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "bt_hfp.h"
|
||||
#include "bt_manager.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_hf_ag_api.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/ringbuf.h"
|
||||
|
||||
static const char* TAG = "BT_HFP";
|
||||
|
||||
// Audio Buffer
|
||||
#define AUDIO_RINGBUF_SIZE (8 * 1024)
|
||||
static RingbufHandle_t s_audio_out_ringbuf = NULL;
|
||||
|
||||
// State
|
||||
static bool s_initialized = false;
|
||||
static bool s_audio_connected = false;
|
||||
static esp_bd_addr_t s_connected_peer;
|
||||
static bool s_service_connected = false;
|
||||
|
||||
// External notifications (definiert in bt_manager.c)
|
||||
extern void bt_manager_notify_connected(const esp_bd_addr_t address);
|
||||
extern void bt_manager_notify_disconnected(const esp_bd_addr_t address);
|
||||
extern void bt_manager_notify_button(bt_button_event_t event);
|
||||
extern void bt_manager_notify_audio_data(const uint8_t* data, size_t len);
|
||||
|
||||
// HFP AG Callback
|
||||
static void hf_ag_callback(esp_hf_cb_event_t event, esp_hf_cb_param_t *param)
|
||||
{
|
||||
switch (event) {
|
||||
case ESP_HF_CONNECTION_STATE_EVT:
|
||||
if (param->conn_stat.state == ESP_HF_CONNECTION_STATE_CONNECTED) {
|
||||
ESP_LOGI(TAG, "HFP Service verbunden");
|
||||
memcpy(s_connected_peer, param->conn_stat.remote_bda, ESP_BD_ADDR_LEN);
|
||||
s_service_connected = true;
|
||||
bt_manager_notify_connected(param->conn_stat.remote_bda);
|
||||
} else if (param->conn_stat.state == ESP_HF_CONNECTION_STATE_DISCONNECTED) {
|
||||
ESP_LOGI(TAG, "HFP Service getrennt");
|
||||
s_service_connected = false;
|
||||
s_audio_connected = false;
|
||||
bt_manager_notify_disconnected(param->conn_stat.remote_bda);
|
||||
} else if (param->conn_stat.state == ESP_HF_CONNECTION_STATE_SLC_CONNECTED) {
|
||||
ESP_LOGI(TAG, "HFP SLC verbunden (Service Level Connection)");
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_HF_AUDIO_STATE_EVT:
|
||||
if (param->audio_stat.state == ESP_HF_AUDIO_STATE_CONNECTED) {
|
||||
ESP_LOGI(TAG, "HFP Audio verbunden (SCO)");
|
||||
s_audio_connected = true;
|
||||
} else if (param->audio_stat.state == ESP_HF_AUDIO_STATE_DISCONNECTED) {
|
||||
ESP_LOGI(TAG, "HFP Audio getrennt");
|
||||
s_audio_connected = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_HF_BVRA_EVT:
|
||||
// Voice Recognition aktiviert/deaktiviert
|
||||
ESP_LOGI(TAG, "Voice Recognition: %s",
|
||||
param->vra_rep.value ? "aktiviert" : "deaktiviert");
|
||||
break;
|
||||
|
||||
case ESP_HF_VOLUME_CONTROL_EVT:
|
||||
ESP_LOGI(TAG, "Volume %s: %d",
|
||||
param->volume_control.type == ESP_HF_VOLUME_CONTROL_TARGET_SPK ?
|
||||
"Speaker" : "Mic",
|
||||
param->volume_control.volume);
|
||||
break;
|
||||
|
||||
case ESP_HF_UNAT_RESPONSE_EVT:
|
||||
// Unknown AT Command
|
||||
ESP_LOGD(TAG, "Unbekannter AT Command: %s", param->unat_rep.unat);
|
||||
break;
|
||||
|
||||
case ESP_HF_CIND_RESPONSE_EVT:
|
||||
// Indicator Status Request
|
||||
ESP_LOGD(TAG, "CIND Request");
|
||||
// Antworten mit Standard-Werten
|
||||
esp_hf_ag_cind_response(s_connected_peer,
|
||||
1, // call (0=no call, 1=call)
|
||||
0, // call_setup (0=none, 1=incoming, 2=outgoing)
|
||||
1, // service (0=no service, 1=service)
|
||||
5, // signal (0-5)
|
||||
0, // roam (0=not roaming, 1=roaming)
|
||||
5, // batt (0-5)
|
||||
0); // call_held (0=none, 1=held, 2=hold+active)
|
||||
break;
|
||||
|
||||
case ESP_HF_CLCC_RESPONSE_EVT:
|
||||
// Call List Request - keine aktiven Anrufe melden
|
||||
ESP_LOGD(TAG, "CLCC Request");
|
||||
esp_hf_ag_clcc_response(s_connected_peer, 0, 0, 0, 0, 0, NULL);
|
||||
break;
|
||||
|
||||
case ESP_HF_COPS_RESPONSE_EVT:
|
||||
// Network Operator Request
|
||||
ESP_LOGD(TAG, "COPS Request");
|
||||
esp_hf_ag_cops_response(s_connected_peer, "SIP Phone");
|
||||
break;
|
||||
|
||||
case ESP_HF_CNUM_RESPONSE_EVT:
|
||||
// Subscriber Number Request
|
||||
ESP_LOGD(TAG, "CNUM Request");
|
||||
esp_hf_ag_cnum_response(s_connected_peer, NULL, 0);
|
||||
break;
|
||||
|
||||
case ESP_HF_VTS_RESPONSE_EVT:
|
||||
// DTMF Tone
|
||||
ESP_LOGI(TAG, "DTMF: %s", param->vts_rep.code);
|
||||
break;
|
||||
|
||||
case ESP_HF_NREC_RESPONSE_EVT:
|
||||
// Noise Reduction / Echo Cancellation
|
||||
ESP_LOGI(TAG, "NREC: %s", param->nrec.state ? "an" : "aus");
|
||||
break;
|
||||
|
||||
case ESP_HF_ATA_RESPONSE_EVT:
|
||||
// Answer Call (ATA)
|
||||
ESP_LOGI(TAG, "Headset: Anruf annehmen");
|
||||
bt_manager_notify_button(BT_BUTTON_ANSWER);
|
||||
break;
|
||||
|
||||
case ESP_HF_CHUP_RESPONSE_EVT:
|
||||
// Hangup Call (AT+CHUP)
|
||||
ESP_LOGI(TAG, "Headset: Auflegen");
|
||||
bt_manager_notify_button(BT_BUTTON_HANGUP);
|
||||
break;
|
||||
|
||||
case ESP_HF_DIAL_EVT:
|
||||
// Dial (ATD, ATD>, ATD>mem)
|
||||
if (param->out_call.type == ESP_HF_DIAL_MEM) {
|
||||
ESP_LOGI(TAG, "Headset: Wähle Speicher %d", param->out_call.num_or_loc);
|
||||
} else if (param->out_call.type == ESP_HF_DIAL_VOIP) {
|
||||
ESP_LOGI(TAG, "Headset: Wähle %s", param->out_call.num_or_loc);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Headset: Wahlwiederholung");
|
||||
bt_manager_notify_button(BT_BUTTON_REDIAL);
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_HF_WBS_RESPONSE_EVT:
|
||||
// Wide Band Speech (mSBC Codec)
|
||||
ESP_LOGI(TAG, "WBS Codec: %s",
|
||||
param->wbs_rep.codec == ESP_HF_WBS_PLCM ? "mSBC" : "CVSD");
|
||||
break;
|
||||
|
||||
case ESP_HF_BCS_RESPONSE_EVT:
|
||||
// Codec Selection
|
||||
ESP_LOGI(TAG, "Codec Selected: %d", param->bcs_rep.mode);
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGD(TAG, "HFP Event: %d", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Audio Data Callback (eingehend vom Headset - Mikrofon)
|
||||
static uint32_t hf_ag_incoming_data_callback(uint8_t *buf, uint32_t len)
|
||||
{
|
||||
// Audio-Daten vom Headset-Mikrofon weiterleiten
|
||||
bt_manager_notify_audio_data(buf, len);
|
||||
return len;
|
||||
}
|
||||
|
||||
// Audio Data Request (ausgehend zum Headset - Speaker)
|
||||
static void hf_ag_outgoing_data_callback(uint8_t *buf, uint32_t len)
|
||||
{
|
||||
if (s_audio_out_ringbuf) {
|
||||
size_t item_size;
|
||||
uint8_t* data = xRingbufferReceiveUpTo(s_audio_out_ringbuf, &item_size, 0, len);
|
||||
if (data && item_size > 0) {
|
||||
memcpy(buf, data, item_size);
|
||||
vRingbufferReturnItem(s_audio_out_ringbuf, data);
|
||||
|
||||
// Rest mit Stille füllen
|
||||
if (item_size < len) {
|
||||
memset(buf + item_size, 0, len - item_size);
|
||||
}
|
||||
} else {
|
||||
// Keine Daten - Stille senden
|
||||
memset(buf, 0, len);
|
||||
}
|
||||
} else {
|
||||
memset(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initialisiere HFP Audio Gateway");
|
||||
|
||||
// Audio Output Buffer erstellen
|
||||
s_audio_out_ringbuf = xRingbufferCreate(AUDIO_RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
|
||||
if (!s_audio_out_ringbuf) {
|
||||
ESP_LOGE(TAG, "Ringbuffer erstellen fehlgeschlagen");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// HFP AG initialisieren
|
||||
esp_err_t ret = esp_hf_ag_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HFP AG init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Callback registrieren
|
||||
ret = esp_hf_ag_register_callback(hf_ag_callback);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HFP AG callback register failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Audio Data Callbacks registrieren
|
||||
esp_hf_ag_register_data_callback(hf_ag_incoming_data_callback,
|
||||
hf_ag_outgoing_data_callback);
|
||||
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "HFP Audio Gateway initialisiert");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_deinit(void)
|
||||
{
|
||||
if (!s_initialized) return ESP_OK;
|
||||
|
||||
ESP_LOGI(TAG, "Deinitalisiere HFP");
|
||||
|
||||
if (s_audio_connected) {
|
||||
bt_hfp_audio_disconnect();
|
||||
}
|
||||
|
||||
if (s_service_connected) {
|
||||
esp_hf_ag_slc_disconnect(s_connected_peer);
|
||||
}
|
||||
|
||||
esp_hf_ag_deinit();
|
||||
|
||||
if (s_audio_out_ringbuf) {
|
||||
vRingbufferDelete(s_audio_out_ringbuf);
|
||||
s_audio_out_ringbuf = NULL;
|
||||
}
|
||||
|
||||
s_initialized = false;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_connect(const esp_bd_addr_t address)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
char addr_str[18];
|
||||
bt_addr_to_str(address, addr_str, sizeof(addr_str));
|
||||
ESP_LOGI(TAG, "HFP Connect: %s", addr_str);
|
||||
|
||||
return esp_hf_ag_slc_connect(address);
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_disconnect(const esp_bd_addr_t address)
|
||||
{
|
||||
if (!s_initialized || !s_service_connected) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (s_audio_connected) {
|
||||
bt_hfp_audio_disconnect();
|
||||
}
|
||||
|
||||
return esp_hf_ag_slc_disconnect(address);
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_audio_connect(void)
|
||||
{
|
||||
if (!s_initialized || !s_service_connected) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (s_audio_connected) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starte SCO Audio...");
|
||||
return esp_hf_ag_audio_connect(s_connected_peer);
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_audio_disconnect(void)
|
||||
{
|
||||
if (!s_initialized || !s_audio_connected) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stoppe SCO Audio...");
|
||||
return esp_hf_ag_audio_disconnect(s_connected_peer);
|
||||
}
|
||||
|
||||
esp_err_t bt_hfp_send_audio(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (!s_initialized || !s_audio_connected || !s_audio_out_ringbuf) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// In Ringbuffer schreiben
|
||||
if (xRingbufferSend(s_audio_out_ringbuf, data, len, 0) != pdTRUE) {
|
||||
// Buffer voll - alte Daten verwerfen
|
||||
ESP_LOGW(TAG, "Audio buffer overflow");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_bt_defs.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Initialisiert das HFP Audio Gateway Profil
|
||||
*/
|
||||
esp_err_t bt_hfp_init(void);
|
||||
|
||||
/**
|
||||
* Deinitalisiert HFP
|
||||
*/
|
||||
esp_err_t bt_hfp_deinit(void);
|
||||
|
||||
/**
|
||||
* Verbindet HFP mit einem Gerät
|
||||
*/
|
||||
esp_err_t bt_hfp_connect(const esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Trennt HFP-Verbindung
|
||||
*/
|
||||
esp_err_t bt_hfp_disconnect(const esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Startet Audio-Streaming (SCO)
|
||||
*/
|
||||
esp_err_t bt_hfp_audio_connect(void);
|
||||
|
||||
/**
|
||||
* Stoppt Audio-Streaming
|
||||
*/
|
||||
esp_err_t bt_hfp_audio_disconnect(void);
|
||||
|
||||
/**
|
||||
* Sendet Audio-Daten
|
||||
*/
|
||||
esp_err_t bt_hfp_send_audio(const uint8_t* data, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,455 @@
|
|||
/**
|
||||
* Bluetooth Manager - Verwaltet Bluetooth Classic Headsets
|
||||
*
|
||||
* Unterstützt HFP (Hands-Free Profile) für Headsets
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "bt_manager.h"
|
||||
#include "bt_hfp.h"
|
||||
#include "config/config_manager.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_bt.h"
|
||||
#include "esp_bt_main.h"
|
||||
#include "esp_bt_device.h"
|
||||
#include "esp_gap_bt_api.h"
|
||||
#include "esp_hf_ag_api.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
static const char* TAG = "BT_MGR";
|
||||
|
||||
// State
|
||||
static bool s_initialized = false;
|
||||
static bool s_discovering = false;
|
||||
static esp_bd_addr_t s_connected_device;
|
||||
static bool s_device_connected = false;
|
||||
|
||||
// Callbacks
|
||||
static bt_device_callback_t s_device_callback = NULL;
|
||||
static bt_discovery_callback_t s_discovery_callback = NULL;
|
||||
static bt_audio_callback_t s_audio_callback = NULL;
|
||||
static bt_button_callback_t s_button_callback = NULL;
|
||||
static bt_audio_data_callback_t s_audio_data_callback = NULL;
|
||||
|
||||
// GAP Callback
|
||||
static void gap_callback(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
|
||||
{
|
||||
switch (event) {
|
||||
case ESP_BT_GAP_DISC_RES_EVT: {
|
||||
// Gerät gefunden
|
||||
if (s_discovery_callback) {
|
||||
bt_discovered_device_t dev;
|
||||
memcpy(dev.address, param->disc_res.bda, ESP_BD_ADDR_LEN);
|
||||
dev.rssi = 0;
|
||||
dev.cod = 0;
|
||||
dev.is_headset = false;
|
||||
dev.name[0] = '\0';
|
||||
|
||||
for (int i = 0; i < param->disc_res.num_prop; i++) {
|
||||
esp_bt_gap_dev_prop_t* prop = ¶m->disc_res.prop[i];
|
||||
|
||||
switch (prop->type) {
|
||||
case ESP_BT_GAP_DEV_PROP_BDNAME:
|
||||
strncpy(dev.name, (char*)prop->val, sizeof(dev.name) - 1);
|
||||
break;
|
||||
case ESP_BT_GAP_DEV_PROP_RSSI:
|
||||
dev.rssi = *(int8_t*)prop->val;
|
||||
break;
|
||||
case ESP_BT_GAP_DEV_PROP_COD:
|
||||
dev.cod = *(uint32_t*)prop->val;
|
||||
// Check if headset (Major Device Class: Audio/Video)
|
||||
if (((dev.cod >> 8) & 0x1F) == 0x04) {
|
||||
dev.is_headset = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dev.is_headset || dev.name[0] != '\0') {
|
||||
char addr_str[18];
|
||||
bt_addr_to_str(dev.address, addr_str, sizeof(addr_str));
|
||||
ESP_LOGI(TAG, "Gerät gefunden: %s [%s] RSSI: %d",
|
||||
dev.name, addr_str, dev.rssi);
|
||||
s_discovery_callback(&dev);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
|
||||
if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) {
|
||||
ESP_LOGI(TAG, "Discovery beendet");
|
||||
s_discovering = false;
|
||||
} else if (param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STARTED) {
|
||||
ESP_LOGI(TAG, "Discovery gestartet");
|
||||
s_discovering = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_BT_GAP_AUTH_CMPL_EVT:
|
||||
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
|
||||
ESP_LOGI(TAG, "Authentifizierung erfolgreich: %s", param->auth_cmpl.device_name);
|
||||
|
||||
// Gerät in Config speichern
|
||||
bt_device_config_t dev_cfg;
|
||||
bt_addr_to_str(param->auth_cmpl.bda, dev_cfg.address, CONFIG_MAX_BT_ADDR_LEN);
|
||||
strncpy(dev_cfg.name, (char*)param->auth_cmpl.device_name, CONFIG_MAX_BT_NAME_LEN);
|
||||
dev_cfg.paired = true;
|
||||
dev_cfg.auto_connect = true;
|
||||
dev_cfg.priority = 0;
|
||||
config_save_bt_device(&dev_cfg);
|
||||
|
||||
if (s_device_callback) {
|
||||
s_device_callback(BT_DEVICE_STATE_PAIRED, param->auth_cmpl.bda);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Authentifizierung fehlgeschlagen: %d", param->auth_cmpl.stat);
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_BT_GAP_PIN_REQ_EVT:
|
||||
// PIN Request - Standard PIN "0000"
|
||||
ESP_LOGI(TAG, "PIN Request für: %s", param->pin_req.bda);
|
||||
esp_bt_pin_code_t pin = {'0', '0', '0', '0'};
|
||||
esp_bt_gap_pin_reply(param->pin_req.bda, true, 4, pin);
|
||||
break;
|
||||
|
||||
case ESP_BT_GAP_CFM_REQ_EVT:
|
||||
// Numeric Comparison - automatisch bestätigen
|
||||
ESP_LOGI(TAG, "Numeric Comparison: %lu", param->cfm_req.num_val);
|
||||
esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
|
||||
break;
|
||||
|
||||
case ESP_BT_GAP_KEY_NOTIF_EVT:
|
||||
ESP_LOGI(TAG, "Passkey: %06lu", param->key_notif.passkey);
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGD(TAG, "GAP Event: %d", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initialisiere Bluetooth Manager");
|
||||
|
||||
// Bluetooth Controller konfigurieren
|
||||
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
||||
bt_cfg.mode = ESP_BT_MODE_BTDM; // Dual Mode für Classic + BLE
|
||||
|
||||
esp_err_t ret = esp_bt_controller_init(&bt_cfg);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "BT Controller init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "BT Controller enable failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Bluedroid initialisieren
|
||||
ret = esp_bluedroid_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Bluedroid init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = esp_bluedroid_enable();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Bluedroid enable failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// GAP Callback registrieren
|
||||
ret = esp_bt_gap_register_callback(gap_callback);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GAP callback register failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Gerätename setzen
|
||||
const device_config_t* config = config_get();
|
||||
esp_bt_dev_set_device_name(config->bluetooth.device_name);
|
||||
|
||||
// SSP (Secure Simple Pairing) aktivieren
|
||||
esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE;
|
||||
esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO; // Display + Yes/No
|
||||
esp_bt_gap_set_security_param(param_type, &iocap, sizeof(esp_bt_io_cap_t));
|
||||
|
||||
// PIN Code Support für ältere Geräte
|
||||
esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_VARIABLE;
|
||||
esp_bt_pin_code_t pin_code = {'0', '0', '0', '0'};
|
||||
esp_bt_gap_set_pin(pin_type, 4, pin_code);
|
||||
|
||||
// HFP initialisieren
|
||||
ret = bt_hfp_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HFP init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Sichtbar machen
|
||||
bt_manager_set_discoverable(true);
|
||||
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "Bluetooth Manager initialisiert");
|
||||
|
||||
// Auto-Connect für gepaarte Geräte
|
||||
if (config->bluetooth.device_count > 0) {
|
||||
ESP_LOGI(TAG, "Versuche Auto-Connect für %d Geräte...",
|
||||
config->bluetooth.device_count);
|
||||
|
||||
for (int i = 0; i < config->bluetooth.device_count; i++) {
|
||||
if (config->bluetooth.devices[i].auto_connect) {
|
||||
esp_bd_addr_t addr;
|
||||
if (bt_str_to_addr(config->bluetooth.devices[i].address, addr) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Auto-Connect: %s", config->bluetooth.devices[i].name);
|
||||
bt_manager_connect(addr);
|
||||
break; // Nur ein Gerät gleichzeitig
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_deinit(void)
|
||||
{
|
||||
if (!s_initialized) return ESP_OK;
|
||||
|
||||
ESP_LOGI(TAG, "Deinitalisiere Bluetooth Manager");
|
||||
|
||||
bt_hfp_deinit();
|
||||
|
||||
esp_bluedroid_disable();
|
||||
esp_bluedroid_deinit();
|
||||
esp_bt_controller_disable();
|
||||
esp_bt_controller_deinit();
|
||||
|
||||
s_initialized = false;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_start_discovery(void)
|
||||
{
|
||||
if (s_discovering) {
|
||||
ESP_LOGW(TAG, "Discovery bereits aktiv");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starte Bluetooth-Suche");
|
||||
|
||||
// Inquiry-Mode: RSSI + Extended Inquiry Response
|
||||
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
|
||||
|
||||
return esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 10, 0);
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_stop_discovery(void)
|
||||
{
|
||||
if (!s_discovering) return ESP_OK;
|
||||
|
||||
ESP_LOGI(TAG, "Stoppe Bluetooth-Suche");
|
||||
return esp_bt_gap_cancel_discovery();
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_pair(const esp_bd_addr_t address)
|
||||
{
|
||||
char addr_str[18];
|
||||
bt_addr_to_str(address, addr_str, sizeof(addr_str));
|
||||
ESP_LOGI(TAG, "Starte Pairing mit: %s", addr_str);
|
||||
|
||||
// Stoppe Discovery falls aktiv
|
||||
if (s_discovering) {
|
||||
esp_bt_gap_cancel_discovery();
|
||||
}
|
||||
|
||||
if (s_device_callback) {
|
||||
s_device_callback(BT_DEVICE_STATE_PAIRING, address);
|
||||
}
|
||||
|
||||
// Verbindung initiiert das Pairing
|
||||
return bt_hfp_connect(address);
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_unpair(const esp_bd_addr_t address)
|
||||
{
|
||||
char addr_str[18];
|
||||
bt_addr_to_str(address, addr_str, sizeof(addr_str));
|
||||
ESP_LOGI(TAG, "Entferne Gerät: %s", addr_str);
|
||||
|
||||
// Trennen falls verbunden
|
||||
if (s_device_connected && memcmp(s_connected_device, address, ESP_BD_ADDR_LEN) == 0) {
|
||||
bt_manager_disconnect(address);
|
||||
}
|
||||
|
||||
// Aus Bonding-Liste entfernen
|
||||
return esp_bt_gap_remove_bond_device((uint8_t*)address);
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_connect(const esp_bd_addr_t address)
|
||||
{
|
||||
char addr_str[18];
|
||||
bt_addr_to_str(address, addr_str, sizeof(addr_str));
|
||||
ESP_LOGI(TAG, "Verbinde mit: %s", addr_str);
|
||||
|
||||
if (s_device_callback) {
|
||||
s_device_callback(BT_DEVICE_STATE_CONNECTING, address);
|
||||
}
|
||||
|
||||
return bt_hfp_connect(address);
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_disconnect(const esp_bd_addr_t address)
|
||||
{
|
||||
char addr_str[18];
|
||||
bt_addr_to_str(address, addr_str, sizeof(addr_str));
|
||||
ESP_LOGI(TAG, "Trenne Verbindung: %s", addr_str);
|
||||
|
||||
return bt_hfp_disconnect(address);
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_disconnect_all(void)
|
||||
{
|
||||
if (s_device_connected) {
|
||||
return bt_manager_disconnect(s_connected_device);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool bt_manager_is_connected(void)
|
||||
{
|
||||
return s_device_connected;
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_get_connected_device(esp_bd_addr_t address)
|
||||
{
|
||||
if (!s_device_connected) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
memcpy(address, s_connected_device, ESP_BD_ADDR_LEN);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_set_discoverable(bool discoverable)
|
||||
{
|
||||
ESP_LOGI(TAG, "Setze Sichtbarkeit: %s", discoverable ? "An" : "Aus");
|
||||
|
||||
if (discoverable) {
|
||||
return esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
|
||||
} else {
|
||||
return esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t bt_manager_send_audio(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (!s_device_connected) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return bt_hfp_send_audio(data, len);
|
||||
}
|
||||
|
||||
// Callback Registration
|
||||
void bt_manager_register_device_callback(bt_device_callback_t callback)
|
||||
{
|
||||
s_device_callback = callback;
|
||||
}
|
||||
|
||||
void bt_manager_register_discovery_callback(bt_discovery_callback_t callback)
|
||||
{
|
||||
s_discovery_callback = callback;
|
||||
}
|
||||
|
||||
void bt_manager_register_audio_callback(bt_audio_callback_t callback)
|
||||
{
|
||||
s_audio_callback = callback;
|
||||
}
|
||||
|
||||
void bt_manager_register_button_callback(bt_button_callback_t callback)
|
||||
{
|
||||
s_button_callback = callback;
|
||||
}
|
||||
|
||||
void bt_manager_register_audio_data_callback(bt_audio_data_callback_t callback)
|
||||
{
|
||||
s_audio_data_callback = callback;
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
void bt_addr_to_str(const esp_bd_addr_t addr, char* str, size_t len)
|
||||
{
|
||||
snprintf(str, len, "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
|
||||
}
|
||||
|
||||
esp_err_t bt_str_to_addr(const char* str, esp_bd_addr_t addr)
|
||||
{
|
||||
unsigned int values[6];
|
||||
if (sscanf(str, "%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
&values[0], &values[1], &values[2],
|
||||
&values[3], &values[4], &values[5]) != 6) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
addr[i] = (uint8_t)values[i];
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Internal: Wird von bt_hfp aufgerufen
|
||||
void bt_manager_notify_connected(const esp_bd_addr_t address)
|
||||
{
|
||||
memcpy(s_connected_device, address, ESP_BD_ADDR_LEN);
|
||||
s_device_connected = true;
|
||||
|
||||
if (s_device_callback) {
|
||||
s_device_callback(BT_DEVICE_STATE_CONNECTED, address);
|
||||
}
|
||||
if (s_audio_callback) {
|
||||
s_audio_callback(BT_AUDIO_STATE_OPEN);
|
||||
}
|
||||
}
|
||||
|
||||
void bt_manager_notify_disconnected(const esp_bd_addr_t address)
|
||||
{
|
||||
if (memcmp(s_connected_device, address, ESP_BD_ADDR_LEN) == 0) {
|
||||
s_device_connected = false;
|
||||
memset(s_connected_device, 0, ESP_BD_ADDR_LEN);
|
||||
}
|
||||
|
||||
if (s_device_callback) {
|
||||
s_device_callback(BT_DEVICE_STATE_DISCONNECTED, address);
|
||||
}
|
||||
if (s_audio_callback) {
|
||||
s_audio_callback(BT_AUDIO_STATE_IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
void bt_manager_notify_button(bt_button_event_t event)
|
||||
{
|
||||
if (s_button_callback) {
|
||||
s_button_callback(event);
|
||||
}
|
||||
}
|
||||
|
||||
void bt_manager_notify_audio_data(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (s_audio_data_callback) {
|
||||
s_audio_data_callback(data, len);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_bt_defs.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Bluetooth-Gerätestatus
|
||||
typedef enum {
|
||||
BT_DEVICE_STATE_UNKNOWN = 0,
|
||||
BT_DEVICE_STATE_DISCOVERED,
|
||||
BT_DEVICE_STATE_PAIRING,
|
||||
BT_DEVICE_STATE_PAIRED,
|
||||
BT_DEVICE_STATE_CONNECTING,
|
||||
BT_DEVICE_STATE_CONNECTED,
|
||||
BT_DEVICE_STATE_DISCONNECTED
|
||||
} bt_device_state_t;
|
||||
|
||||
// Bluetooth Audio-Status
|
||||
typedef enum {
|
||||
BT_AUDIO_STATE_IDLE = 0,
|
||||
BT_AUDIO_STATE_OPENING,
|
||||
BT_AUDIO_STATE_OPEN,
|
||||
BT_AUDIO_STATE_STREAMING
|
||||
} bt_audio_state_t;
|
||||
|
||||
// Entdecktes Bluetooth-Gerät
|
||||
typedef struct {
|
||||
esp_bd_addr_t address;
|
||||
char name[32];
|
||||
int rssi;
|
||||
uint32_t cod; // Class of Device
|
||||
bool is_headset;
|
||||
} bt_discovered_device_t;
|
||||
|
||||
// Headset-Button Events
|
||||
typedef enum {
|
||||
BT_BUTTON_NONE = 0,
|
||||
BT_BUTTON_ANSWER, // Anruf annehmen
|
||||
BT_BUTTON_REJECT, // Anruf ablehnen
|
||||
BT_BUTTON_HANGUP, // Anruf beenden
|
||||
BT_BUTTON_REDIAL, // Wahlwiederholung
|
||||
BT_BUTTON_VOLUME_UP,
|
||||
BT_BUTTON_VOLUME_DOWN,
|
||||
BT_BUTTON_MUTE
|
||||
} bt_button_event_t;
|
||||
|
||||
// Event-Callbacks
|
||||
typedef void (*bt_device_callback_t)(bt_device_state_t state, const esp_bd_addr_t address);
|
||||
typedef void (*bt_discovery_callback_t)(const bt_discovered_device_t* device);
|
||||
typedef void (*bt_audio_callback_t)(bt_audio_state_t state);
|
||||
typedef void (*bt_button_callback_t)(bt_button_event_t event);
|
||||
typedef void (*bt_audio_data_callback_t)(const uint8_t* data, size_t len);
|
||||
|
||||
/**
|
||||
* Initialisiert den Bluetooth-Manager
|
||||
*/
|
||||
esp_err_t bt_manager_init(void);
|
||||
|
||||
/**
|
||||
* Deinitalisiert den Bluetooth-Manager
|
||||
*/
|
||||
esp_err_t bt_manager_deinit(void);
|
||||
|
||||
/**
|
||||
* Startet die Bluetooth-Gerätesuche
|
||||
*/
|
||||
esp_err_t bt_manager_start_discovery(void);
|
||||
|
||||
/**
|
||||
* Stoppt die Bluetooth-Gerätesuche
|
||||
*/
|
||||
esp_err_t bt_manager_stop_discovery(void);
|
||||
|
||||
/**
|
||||
* Pairt mit einem Gerät
|
||||
*/
|
||||
esp_err_t bt_manager_pair(const esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Entfernt ein gepairtes Gerät
|
||||
*/
|
||||
esp_err_t bt_manager_unpair(const esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Verbindet mit einem gepairten Headset
|
||||
*/
|
||||
esp_err_t bt_manager_connect(const esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Trennt die Verbindung zu einem Headset
|
||||
*/
|
||||
esp_err_t bt_manager_disconnect(const esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Trennt alle verbundenen Geräte
|
||||
*/
|
||||
esp_err_t bt_manager_disconnect_all(void);
|
||||
|
||||
/**
|
||||
* Gibt zurück ob ein Headset verbunden ist
|
||||
*/
|
||||
bool bt_manager_is_connected(void);
|
||||
|
||||
/**
|
||||
* Gibt die Adresse des verbundenen Headsets zurück
|
||||
*/
|
||||
esp_err_t bt_manager_get_connected_device(esp_bd_addr_t address);
|
||||
|
||||
/**
|
||||
* Setzt die Sichtbarkeit
|
||||
*/
|
||||
esp_err_t bt_manager_set_discoverable(bool discoverable);
|
||||
|
||||
/**
|
||||
* Sendet Audio-Daten zum Headset
|
||||
*/
|
||||
esp_err_t bt_manager_send_audio(const uint8_t* data, size_t len);
|
||||
|
||||
/**
|
||||
* Registriert Callbacks
|
||||
*/
|
||||
void bt_manager_register_device_callback(bt_device_callback_t callback);
|
||||
void bt_manager_register_discovery_callback(bt_discovery_callback_t callback);
|
||||
void bt_manager_register_audio_callback(bt_audio_callback_t callback);
|
||||
void bt_manager_register_button_callback(bt_button_callback_t callback);
|
||||
void bt_manager_register_audio_data_callback(bt_audio_data_callback_t callback);
|
||||
|
||||
/**
|
||||
* Konvertiert BD_ADDR zu String
|
||||
*/
|
||||
void bt_addr_to_str(const esp_bd_addr_t addr, char* str, size_t len);
|
||||
|
||||
/**
|
||||
* Konvertiert String zu BD_ADDR
|
||||
*/
|
||||
esp_err_t bt_str_to_addr(const char* str, esp_bd_addr_t addr);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* Config Manager - NVS-basierte Konfigurationsverwaltung
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "config_manager.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
static const char* TAG = "CONFIG";
|
||||
|
||||
// NVS Namespace
|
||||
#define NVS_NAMESPACE "bsc_config"
|
||||
|
||||
// NVS Keys
|
||||
#define KEY_WIFI_SSID "wifi_ssid"
|
||||
#define KEY_WIFI_PASS "wifi_pass"
|
||||
#define KEY_WIFI_IP_MODE "wifi_ip_mode"
|
||||
#define KEY_WIFI_STATIC_IP "wifi_static_ip"
|
||||
#define KEY_WIFI_GATEWAY "wifi_gw"
|
||||
#define KEY_WIFI_NETMASK "wifi_nm"
|
||||
#define KEY_WIFI_DNS "wifi_dns"
|
||||
|
||||
#define KEY_SIP_SERVER "sip_server"
|
||||
#define KEY_SIP_PORT "sip_port"
|
||||
#define KEY_SIP_USER "sip_user"
|
||||
#define KEY_SIP_PASS "sip_pass"
|
||||
#define KEY_SIP_DISPLAY "sip_display"
|
||||
|
||||
#define KEY_BT_NAME "bt_name"
|
||||
#define KEY_BT_DISCOVERABLE "bt_disc"
|
||||
#define KEY_BT_DEV_COUNT "bt_dev_cnt"
|
||||
#define KEY_BT_DEV_PREFIX "bt_dev_" // bt_dev_0, bt_dev_1, ...
|
||||
|
||||
// Aktuelle Konfiguration
|
||||
static device_config_t s_config;
|
||||
static bool s_initialized = false;
|
||||
|
||||
// NVS Handle
|
||||
static nvs_handle_t s_nvs_handle;
|
||||
|
||||
// Hilfsfunktionen
|
||||
static esp_err_t load_string(const char* key, char* buf, size_t max_len)
|
||||
{
|
||||
size_t len = max_len;
|
||||
esp_err_t err = nvs_get_str(s_nvs_handle, key, buf, &len);
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
buf[0] = '\0';
|
||||
return ESP_OK;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t save_string(const char* key, const char* value)
|
||||
{
|
||||
return nvs_set_str(s_nvs_handle, key, value);
|
||||
}
|
||||
|
||||
static esp_err_t load_wifi_config(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
err = load_string(KEY_WIFI_SSID, s_config.wifi.ssid, CONFIG_MAX_SSID_LEN);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = load_string(KEY_WIFI_PASS, s_config.wifi.password, CONFIG_MAX_PASSWORD_LEN);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint8_t ip_mode = 0;
|
||||
err = nvs_get_u8(s_nvs_handle, KEY_WIFI_IP_MODE, &ip_mode);
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
s_config.wifi.ip_mode = IP_MODE_DHCP;
|
||||
} else if (err == ESP_OK) {
|
||||
s_config.wifi.ip_mode = (ip_mode_t)ip_mode;
|
||||
} else {
|
||||
return err;
|
||||
}
|
||||
|
||||
load_string(KEY_WIFI_STATIC_IP, s_config.wifi.static_ip, CONFIG_MAX_IP_LEN);
|
||||
load_string(KEY_WIFI_GATEWAY, s_config.wifi.gateway, CONFIG_MAX_IP_LEN);
|
||||
load_string(KEY_WIFI_NETMASK, s_config.wifi.netmask, CONFIG_MAX_IP_LEN);
|
||||
load_string(KEY_WIFI_DNS, s_config.wifi.dns, CONFIG_MAX_IP_LEN);
|
||||
|
||||
// Konfiguriert wenn SSID vorhanden
|
||||
s_config.wifi.configured = (strlen(s_config.wifi.ssid) > 0);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t load_sip_config(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
err = load_string(KEY_SIP_SERVER, s_config.sip.server, CONFIG_MAX_SIP_SERVER_LEN);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
uint16_t port = CONFIG_BSC_SIP_DEFAULT_PORT;
|
||||
nvs_get_u16(s_nvs_handle, KEY_SIP_PORT, &port);
|
||||
s_config.sip.port = port;
|
||||
|
||||
load_string(KEY_SIP_USER, s_config.sip.username, CONFIG_MAX_SIP_USER_LEN);
|
||||
load_string(KEY_SIP_PASS, s_config.sip.password, CONFIG_MAX_PASSWORD_LEN);
|
||||
load_string(KEY_SIP_DISPLAY, s_config.sip.display_name, CONFIG_MAX_SIP_USER_LEN);
|
||||
|
||||
// Konfiguriert wenn Server und User vorhanden
|
||||
s_config.sip.configured = (strlen(s_config.sip.server) > 0 &&
|
||||
strlen(s_config.sip.username) > 0);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t load_bt_config(void)
|
||||
{
|
||||
// Bluetooth-Gerätename
|
||||
esp_err_t err = load_string(KEY_BT_NAME, s_config.bluetooth.device_name, CONFIG_MAX_BT_NAME_LEN);
|
||||
if (err != ESP_OK || strlen(s_config.bluetooth.device_name) == 0) {
|
||||
strncpy(s_config.bluetooth.device_name, CONFIG_BSC_BT_DEVICE_NAME, CONFIG_MAX_BT_NAME_LEN);
|
||||
}
|
||||
|
||||
// Discoverable Status
|
||||
uint8_t disc = 1;
|
||||
nvs_get_u8(s_nvs_handle, KEY_BT_DISCOVERABLE, &disc);
|
||||
s_config.bluetooth.discoverable = (disc != 0);
|
||||
|
||||
// Gepaarte Geräte laden
|
||||
uint8_t dev_count = 0;
|
||||
nvs_get_u8(s_nvs_handle, KEY_BT_DEV_COUNT, &dev_count);
|
||||
s_config.bluetooth.device_count = 0;
|
||||
|
||||
for (int i = 0; i < dev_count && i < CONFIG_BSC_MAX_BT_DEVICES; i++) {
|
||||
char key[32];
|
||||
char data[128];
|
||||
|
||||
snprintf(key, sizeof(key), "%s%d", KEY_BT_DEV_PREFIX, i);
|
||||
size_t len = sizeof(data);
|
||||
|
||||
if (nvs_get_str(s_nvs_handle, key, data, &len) == ESP_OK) {
|
||||
// Format: "address|name|auto_connect|priority"
|
||||
bt_device_config_t* dev = &s_config.bluetooth.devices[s_config.bluetooth.device_count];
|
||||
|
||||
char* token = strtok(data, "|");
|
||||
if (token) strncpy(dev->address, token, CONFIG_MAX_BT_ADDR_LEN);
|
||||
|
||||
token = strtok(NULL, "|");
|
||||
if (token) strncpy(dev->name, token, CONFIG_MAX_BT_NAME_LEN);
|
||||
|
||||
token = strtok(NULL, "|");
|
||||
if (token) dev->auto_connect = (atoi(token) != 0);
|
||||
|
||||
token = strtok(NULL, "|");
|
||||
if (token) dev->priority = (uint8_t)atoi(token);
|
||||
|
||||
dev->paired = true;
|
||||
s_config.bluetooth.device_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t save_bt_devices(void)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
// Anzahl speichern
|
||||
err = nvs_set_u8(s_nvs_handle, KEY_BT_DEV_COUNT, s_config.bluetooth.device_count);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// Jedes Gerät speichern
|
||||
for (int i = 0; i < s_config.bluetooth.device_count; i++) {
|
||||
char key[32];
|
||||
char data[128];
|
||||
bt_device_config_t* dev = &s_config.bluetooth.devices[i];
|
||||
|
||||
snprintf(key, sizeof(key), "%s%d", KEY_BT_DEV_PREFIX, i);
|
||||
snprintf(data, sizeof(data), "%s|%s|%d|%d",
|
||||
dev->address, dev->name, dev->auto_connect ? 1 : 0, dev->priority);
|
||||
|
||||
err = nvs_set_str(s_nvs_handle, key, data);
|
||||
if (err != ESP_OK) return err;
|
||||
}
|
||||
|
||||
return nvs_commit(s_nvs_handle);
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
esp_err_t config_manager_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initialisiere Config Manager");
|
||||
|
||||
// NVS öffnen
|
||||
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &s_nvs_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "NVS öffnen fehlgeschlagen: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Konfiguration initialisieren
|
||||
memset(&s_config, 0, sizeof(s_config));
|
||||
|
||||
// Laden
|
||||
err = load_wifi_config();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "WiFi-Config laden fehlgeschlagen: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
err = load_sip_config();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "SIP-Config laden fehlgeschlagen: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
err = load_bt_config();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "BT-Config laden fehlgeschlagen: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Konfiguration geladen:");
|
||||
ESP_LOGI(TAG, " WiFi: %s", s_config.wifi.configured ? s_config.wifi.ssid : "(nicht konfiguriert)");
|
||||
ESP_LOGI(TAG, " SIP: %s", s_config.sip.configured ? s_config.sip.server : "(nicht konfiguriert)");
|
||||
ESP_LOGI(TAG, " BT: %d gepaarte Geräte", s_config.bluetooth.device_count);
|
||||
|
||||
s_initialized = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const device_config_t* config_get(void)
|
||||
{
|
||||
return &s_config;
|
||||
}
|
||||
|
||||
esp_err_t config_save_wifi(const wifi_config_data_t* wifi_config)
|
||||
{
|
||||
if (!wifi_config) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
ESP_LOGI(TAG, "Speichere WiFi-Konfiguration: SSID=%s", wifi_config->ssid);
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
err = save_string(KEY_WIFI_SSID, wifi_config->ssid);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = save_string(KEY_WIFI_PASS, wifi_config->password);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = nvs_set_u8(s_nvs_handle, KEY_WIFI_IP_MODE, (uint8_t)wifi_config->ip_mode);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
if (wifi_config->ip_mode == IP_MODE_STATIC) {
|
||||
save_string(KEY_WIFI_STATIC_IP, wifi_config->static_ip);
|
||||
save_string(KEY_WIFI_GATEWAY, wifi_config->gateway);
|
||||
save_string(KEY_WIFI_NETMASK, wifi_config->netmask);
|
||||
save_string(KEY_WIFI_DNS, wifi_config->dns);
|
||||
}
|
||||
|
||||
err = nvs_commit(s_nvs_handle);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// Lokale Kopie aktualisieren
|
||||
memcpy(&s_config.wifi, wifi_config, sizeof(wifi_config_data_t));
|
||||
s_config.wifi.configured = (strlen(s_config.wifi.ssid) > 0);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t config_save_sip(const sip_config_data_t* sip_config)
|
||||
{
|
||||
if (!sip_config) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
ESP_LOGI(TAG, "Speichere SIP-Konfiguration: %s@%s:%d",
|
||||
sip_config->username, sip_config->server, sip_config->port);
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
err = save_string(KEY_SIP_SERVER, sip_config->server);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = nvs_set_u16(s_nvs_handle, KEY_SIP_PORT, sip_config->port);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = save_string(KEY_SIP_USER, sip_config->username);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = save_string(KEY_SIP_PASS, sip_config->password);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
save_string(KEY_SIP_DISPLAY, sip_config->display_name);
|
||||
|
||||
err = nvs_commit(s_nvs_handle);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// Lokale Kopie aktualisieren
|
||||
memcpy(&s_config.sip, sip_config, sizeof(sip_config_data_t));
|
||||
s_config.sip.configured = (strlen(s_config.sip.server) > 0 &&
|
||||
strlen(s_config.sip.username) > 0);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t config_save_bt_device(const bt_device_config_t* device)
|
||||
{
|
||||
if (!device) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
// Prüfen ob Gerät bereits existiert
|
||||
for (int i = 0; i < s_config.bluetooth.device_count; i++) {
|
||||
if (strcmp(s_config.bluetooth.devices[i].address, device->address) == 0) {
|
||||
// Update
|
||||
memcpy(&s_config.bluetooth.devices[i], device, sizeof(bt_device_config_t));
|
||||
return save_bt_devices();
|
||||
}
|
||||
}
|
||||
|
||||
// Neues Gerät
|
||||
if (s_config.bluetooth.device_count >= CONFIG_BSC_MAX_BT_DEVICES) {
|
||||
ESP_LOGW(TAG, "Maximale Anzahl BT-Geräte erreicht");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
memcpy(&s_config.bluetooth.devices[s_config.bluetooth.device_count],
|
||||
device, sizeof(bt_device_config_t));
|
||||
s_config.bluetooth.device_count++;
|
||||
|
||||
ESP_LOGI(TAG, "BT-Gerät hinzugefügt: %s (%s)", device->name, device->address);
|
||||
|
||||
return save_bt_devices();
|
||||
}
|
||||
|
||||
esp_err_t config_remove_bt_device(const char* address)
|
||||
{
|
||||
if (!address) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
for (int i = 0; i < s_config.bluetooth.device_count; i++) {
|
||||
if (strcmp(s_config.bluetooth.devices[i].address, address) == 0) {
|
||||
// Gefunden - entfernen durch Verschieben
|
||||
ESP_LOGI(TAG, "Entferne BT-Gerät: %s", address);
|
||||
|
||||
for (int j = i; j < s_config.bluetooth.device_count - 1; j++) {
|
||||
memcpy(&s_config.bluetooth.devices[j],
|
||||
&s_config.bluetooth.devices[j + 1],
|
||||
sizeof(bt_device_config_t));
|
||||
}
|
||||
s_config.bluetooth.device_count--;
|
||||
|
||||
return save_bt_devices();
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
esp_err_t config_clear_bt_devices(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Lösche alle BT-Geräte");
|
||||
s_config.bluetooth.device_count = 0;
|
||||
return save_bt_devices();
|
||||
}
|
||||
|
||||
esp_err_t config_factory_reset(void)
|
||||
{
|
||||
ESP_LOGW(TAG, "Werksreset durchführen!");
|
||||
|
||||
esp_err_t err = nvs_erase_all(s_nvs_handle);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
err = nvs_commit(s_nvs_handle);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// Konfiguration zurücksetzen
|
||||
memset(&s_config, 0, sizeof(s_config));
|
||||
strncpy(s_config.bluetooth.device_name, CONFIG_BSC_BT_DEVICE_NAME, CONFIG_MAX_BT_NAME_LEN);
|
||||
s_config.bluetooth.discoverable = true;
|
||||
s_config.sip.port = CONFIG_BSC_SIP_DEFAULT_PORT;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool config_wifi_is_configured(void)
|
||||
{
|
||||
return s_config.wifi.configured;
|
||||
}
|
||||
|
||||
bool config_sip_is_configured(void)
|
||||
{
|
||||
return s_config.sip.configured;
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Maximale Längen
|
||||
#define CONFIG_MAX_SSID_LEN 32
|
||||
#define CONFIG_MAX_PASSWORD_LEN 64
|
||||
#define CONFIG_MAX_IP_LEN 16
|
||||
#define CONFIG_MAX_SIP_USER_LEN 64
|
||||
#define CONFIG_MAX_SIP_SERVER_LEN 128
|
||||
#define CONFIG_MAX_BT_NAME_LEN 32
|
||||
#define CONFIG_MAX_BT_ADDR_LEN 18 // "XX:XX:XX:XX:XX:XX"
|
||||
|
||||
// IP-Konfigurationsmodus
|
||||
typedef enum {
|
||||
IP_MODE_DHCP = 0,
|
||||
IP_MODE_STATIC = 1
|
||||
} ip_mode_t;
|
||||
|
||||
// WLAN-Konfiguration
|
||||
typedef struct {
|
||||
char ssid[CONFIG_MAX_SSID_LEN + 1];
|
||||
char password[CONFIG_MAX_PASSWORD_LEN + 1];
|
||||
ip_mode_t ip_mode;
|
||||
char static_ip[CONFIG_MAX_IP_LEN + 1];
|
||||
char gateway[CONFIG_MAX_IP_LEN + 1];
|
||||
char netmask[CONFIG_MAX_IP_LEN + 1];
|
||||
char dns[CONFIG_MAX_IP_LEN + 1];
|
||||
bool configured; // true wenn WLAN-Daten vorhanden
|
||||
} wifi_config_data_t;
|
||||
|
||||
// SIP-Konfiguration
|
||||
typedef struct {
|
||||
char server[CONFIG_MAX_SIP_SERVER_LEN + 1];
|
||||
uint16_t port;
|
||||
char username[CONFIG_MAX_SIP_USER_LEN + 1];
|
||||
char password[CONFIG_MAX_PASSWORD_LEN + 1];
|
||||
char display_name[CONFIG_MAX_SIP_USER_LEN + 1];
|
||||
bool configured;
|
||||
} sip_config_data_t;
|
||||
|
||||
// Bluetooth-Gerät
|
||||
typedef struct {
|
||||
char name[CONFIG_MAX_BT_NAME_LEN + 1];
|
||||
char address[CONFIG_MAX_BT_ADDR_LEN + 1];
|
||||
bool paired;
|
||||
bool auto_connect;
|
||||
uint8_t priority; // Niedrigere Zahl = höhere Priorität
|
||||
} bt_device_config_t;
|
||||
|
||||
// Bluetooth-Konfiguration
|
||||
typedef struct {
|
||||
char device_name[CONFIG_MAX_BT_NAME_LEN + 1];
|
||||
bt_device_config_t devices[CONFIG_BSC_MAX_BT_DEVICES];
|
||||
uint8_t device_count;
|
||||
bool discoverable;
|
||||
} bt_config_data_t;
|
||||
|
||||
// Gesamtkonfiguration
|
||||
typedef struct {
|
||||
wifi_config_data_t wifi;
|
||||
sip_config_data_t sip;
|
||||
bt_config_data_t bluetooth;
|
||||
} device_config_t;
|
||||
|
||||
/**
|
||||
* Initialisiert den Config-Manager und lädt gespeicherte Konfiguration
|
||||
*/
|
||||
esp_err_t config_manager_init(void);
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Konfiguration zurück
|
||||
*/
|
||||
const device_config_t* config_get(void);
|
||||
|
||||
/**
|
||||
* Speichert die WLAN-Konfiguration
|
||||
*/
|
||||
esp_err_t config_save_wifi(const wifi_config_data_t* wifi_config);
|
||||
|
||||
/**
|
||||
* Speichert die SIP-Konfiguration
|
||||
*/
|
||||
esp_err_t config_save_sip(const sip_config_data_t* sip_config);
|
||||
|
||||
/**
|
||||
* Speichert ein Bluetooth-Gerät
|
||||
*/
|
||||
esp_err_t config_save_bt_device(const bt_device_config_t* device);
|
||||
|
||||
/**
|
||||
* Entfernt ein Bluetooth-Gerät
|
||||
*/
|
||||
esp_err_t config_remove_bt_device(const char* address);
|
||||
|
||||
/**
|
||||
* Setzt alle Bluetooth-Geräte zurück
|
||||
*/
|
||||
esp_err_t config_clear_bt_devices(void);
|
||||
|
||||
/**
|
||||
* Setzt die gesamte Konfiguration auf Werkseinstellungen zurück
|
||||
*/
|
||||
esp_err_t config_factory_reset(void);
|
||||
|
||||
/**
|
||||
* Prüft ob WLAN konfiguriert ist
|
||||
*/
|
||||
bool config_wifi_is_configured(void);
|
||||
|
||||
/**
|
||||
* Prüft ob SIP konfiguriert ist
|
||||
*/
|
||||
bool config_sip_is_configured(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
/**
|
||||
* ESP32-S3 Bluetooth SIP Client
|
||||
*
|
||||
* SIP-Telefon mit Bluetooth und USB-Headset Unterstützung
|
||||
* Für Thin-Client Umgebungen ohne nativen CTI-Support
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_event.h"
|
||||
|
||||
#include "config/config_manager.h"
|
||||
#include "wifi/wifi_manager.h"
|
||||
#include "web/web_server.h"
|
||||
#include "bluetooth/bt_manager.h"
|
||||
#include "usb_audio/usb_audio_host.h"
|
||||
#include "audio/audio_router.h"
|
||||
#include "sip/sip_client.h"
|
||||
|
||||
static const char* TAG = "MAIN";
|
||||
|
||||
// Event Group für Synchronisation
|
||||
static EventGroupHandle_t s_app_event_group;
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define SIP_REGISTERED_BIT BIT1
|
||||
#define AUDIO_READY_BIT BIT2
|
||||
|
||||
// Callback für WiFi-Status
|
||||
static void wifi_event_handler(wifi_state_t state, void* data)
|
||||
{
|
||||
switch (state) {
|
||||
case WIFI_STATE_AP_STARTED:
|
||||
ESP_LOGI(TAG, "Hotspot gestartet - Konfiguration unter http://192.168.4.1");
|
||||
break;
|
||||
|
||||
case WIFI_STATE_STA_CONNECTED:
|
||||
ESP_LOGI(TAG, "Mit WLAN verbunden");
|
||||
xEventGroupSetBits(s_app_event_group, WIFI_CONNECTED_BIT);
|
||||
break;
|
||||
|
||||
case WIFI_STATE_STA_DISCONNECTED:
|
||||
ESP_LOGW(TAG, "WLAN-Verbindung verloren");
|
||||
xEventGroupClearBits(s_app_event_group, WIFI_CONNECTED_BIT);
|
||||
break;
|
||||
|
||||
case WIFI_STATE_STA_FAILED:
|
||||
ESP_LOGE(TAG, "WLAN-Verbindung fehlgeschlagen - Fallback zu Hotspot");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback für SIP-Registrierung
|
||||
static void sip_reg_handler(sip_reg_state_t state, const char* message)
|
||||
{
|
||||
switch (state) {
|
||||
case SIP_REG_STATE_REGISTERED:
|
||||
ESP_LOGI(TAG, "SIP registriert: %s", message ? message : "OK");
|
||||
xEventGroupSetBits(s_app_event_group, SIP_REGISTERED_BIT);
|
||||
break;
|
||||
|
||||
case SIP_REG_STATE_FAILED:
|
||||
ESP_LOGE(TAG, "SIP Registrierung fehlgeschlagen: %s", message ? message : "Unbekannt");
|
||||
xEventGroupClearBits(s_app_event_group, SIP_REGISTERED_BIT);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback für Anrufe
|
||||
static void sip_call_handler(const sip_call_info_t* call_info)
|
||||
{
|
||||
if (!call_info) return;
|
||||
|
||||
switch (call_info->state) {
|
||||
case SIP_CALL_STATE_INCOMING:
|
||||
ESP_LOGI(TAG, "Eingehender Anruf von: %s <%s>",
|
||||
call_info->remote_name, call_info->remote_number);
|
||||
// Audio-Router signalisieren
|
||||
break;
|
||||
|
||||
case SIP_CALL_STATE_CONNECTED:
|
||||
ESP_LOGI(TAG, "Anruf verbunden mit: %s", call_info->remote_number);
|
||||
audio_router_start_call();
|
||||
break;
|
||||
|
||||
case SIP_CALL_STATE_DISCONNECTED:
|
||||
ESP_LOGI(TAG, "Anruf beendet (Dauer: %lu Sek.)", call_info->duration_sec);
|
||||
audio_router_stop_call();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback für Headset-Tasten (vereinheitlicht USB und Bluetooth)
|
||||
static void headset_button_handler(headset_button_t button, audio_source_t source)
|
||||
{
|
||||
const char* source_name = (source == AUDIO_SOURCE_USB) ? "USB" : "Bluetooth";
|
||||
|
||||
switch (button) {
|
||||
case HEADSET_BUTTON_ANSWER:
|
||||
ESP_LOGI(TAG, "[%s] Anruf annehmen", source_name);
|
||||
if (sip_client_get_call_state() == SIP_CALL_STATE_INCOMING) {
|
||||
sip_client_answer();
|
||||
}
|
||||
break;
|
||||
|
||||
case HEADSET_BUTTON_HANGUP:
|
||||
ESP_LOGI(TAG, "[%s] Anruf beenden", source_name);
|
||||
if (sip_client_get_call_state() != SIP_CALL_STATE_IDLE) {
|
||||
sip_client_hangup();
|
||||
}
|
||||
break;
|
||||
|
||||
case HEADSET_BUTTON_REJECT:
|
||||
ESP_LOGI(TAG, "[%s] Anruf ablehnen", source_name);
|
||||
if (sip_client_get_call_state() == SIP_CALL_STATE_INCOMING) {
|
||||
sip_client_reject();
|
||||
}
|
||||
break;
|
||||
|
||||
case HEADSET_BUTTON_MUTE:
|
||||
ESP_LOGI(TAG, "[%s] Mute toggle", source_name);
|
||||
audio_router_set_mute(!audio_router_is_muted());
|
||||
break;
|
||||
|
||||
case HEADSET_BUTTON_VOLUME_UP:
|
||||
ESP_LOGI(TAG, "[%s] Lauter", source_name);
|
||||
{
|
||||
uint8_t vol = audio_router_get_volume();
|
||||
if (vol < 100) audio_router_set_volume(vol + 10);
|
||||
}
|
||||
break;
|
||||
|
||||
case HEADSET_BUTTON_VOLUME_DOWN:
|
||||
ESP_LOGI(TAG, "[%s] Leiser", source_name);
|
||||
{
|
||||
uint8_t vol = audio_router_get_volume();
|
||||
if (vol > 0) audio_router_set_volume(vol > 10 ? vol - 10 : 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Callback für Audio-Quellenwechsel
|
||||
static void audio_source_change_handler(audio_source_t old_source, audio_source_t new_source)
|
||||
{
|
||||
const char* sources[] = {"Keine", "USB", "Bluetooth"};
|
||||
ESP_LOGI(TAG, "Audio-Quelle gewechselt: %s -> %s",
|
||||
sources[old_source], sources[new_source]);
|
||||
}
|
||||
|
||||
// Audio vom Headset zum SIP-Client
|
||||
static void audio_from_headset_handler(const uint8_t* data, size_t len, const audio_format_t* format)
|
||||
{
|
||||
if (sip_client_get_call_state() == SIP_CALL_STATE_CONNECTED) {
|
||||
sip_client_send_audio(data, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Audio vom SIP-Client zum Headset
|
||||
static void audio_from_sip_handler(const uint8_t* data, size_t len, const sip_audio_format_t* format)
|
||||
{
|
||||
audio_format_t af = {
|
||||
.sample_rate = format->sample_rate,
|
||||
.channels = format->channels,
|
||||
.bits_per_sample = 16
|
||||
};
|
||||
audio_router_send_to_headset(data, len, &af);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "=================================");
|
||||
ESP_LOGI(TAG, "ESP32-S3 Bluetooth SIP Client");
|
||||
ESP_LOGI(TAG, "=================================");
|
||||
|
||||
// Event Group erstellen
|
||||
s_app_event_group = xEventGroupCreate();
|
||||
|
||||
// NVS initialisieren
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_LOGW(TAG, "NVS Partition löschen und neu initialisieren");
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
// Event Loop erstellen
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// ========== Module initialisieren ==========
|
||||
|
||||
// 1. Config Manager (lädt gespeicherte Konfiguration)
|
||||
ESP_LOGI(TAG, "Initialisiere Config Manager...");
|
||||
ESP_ERROR_CHECK(config_manager_init());
|
||||
|
||||
// 2. WiFi Manager
|
||||
ESP_LOGI(TAG, "Initialisiere WiFi Manager...");
|
||||
ESP_ERROR_CHECK(wifi_manager_init());
|
||||
wifi_manager_register_callback(wifi_event_handler);
|
||||
|
||||
// 3. Webserver (läuft immer für Konfiguration)
|
||||
ESP_LOGI(TAG, "Initialisiere Webserver...");
|
||||
ESP_ERROR_CHECK(web_server_init());
|
||||
|
||||
// 4. Audio Router
|
||||
ESP_LOGI(TAG, "Initialisiere Audio Router...");
|
||||
ESP_ERROR_CHECK(audio_router_init());
|
||||
audio_router_register_button_callback(headset_button_handler);
|
||||
audio_router_register_source_change_callback(audio_source_change_handler);
|
||||
audio_router_register_input_callback(audio_from_headset_handler);
|
||||
|
||||
// 5. USB Audio Host
|
||||
ESP_LOGI(TAG, "Initialisiere USB Audio Host...");
|
||||
ESP_ERROR_CHECK(usb_audio_host_init());
|
||||
|
||||
// 6. Bluetooth Manager
|
||||
ESP_LOGI(TAG, "Initialisiere Bluetooth Manager...");
|
||||
ESP_ERROR_CHECK(bt_manager_init());
|
||||
|
||||
// 7. SIP Client
|
||||
ESP_LOGI(TAG, "Initialisiere SIP Client...");
|
||||
ESP_ERROR_CHECK(sip_client_init());
|
||||
sip_client_register_reg_callback(sip_reg_handler);
|
||||
sip_client_register_call_callback(sip_call_handler);
|
||||
sip_client_register_audio_callback(audio_from_sip_handler);
|
||||
|
||||
// ========== Starten ==========
|
||||
|
||||
// WiFi starten (AP oder STA je nach Konfiguration)
|
||||
ESP_LOGI(TAG, "Starte WiFi...");
|
||||
ESP_ERROR_CHECK(wifi_manager_start());
|
||||
|
||||
// Warten auf WLAN-Verbindung wenn konfiguriert
|
||||
if (config_wifi_is_configured()) {
|
||||
ESP_LOGI(TAG, "Warte auf WLAN-Verbindung...");
|
||||
EventBits_t bits = xEventGroupWaitBits(
|
||||
s_app_event_group,
|
||||
WIFI_CONNECTED_BIT,
|
||||
pdFALSE,
|
||||
pdTRUE,
|
||||
pdMS_TO_TICKS(30000) // 30 Sekunden Timeout
|
||||
);
|
||||
|
||||
if (bits & WIFI_CONNECTED_BIT) {
|
||||
// SIP registrieren wenn konfiguriert
|
||||
if (config_sip_is_configured()) {
|
||||
ESP_LOGI(TAG, "Registriere bei TK-Anlage...");
|
||||
sip_client_register();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SIP nicht konfiguriert - bitte über Weboberfläche einrichten");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "WLAN nicht konfiguriert - Hotspot-Modus aktiv");
|
||||
ESP_LOGI(TAG, "Verbinden Sie sich mit '%s' und öffnen Sie http://192.168.4.1",
|
||||
CONFIG_BSC_DEFAULT_AP_SSID);
|
||||
}
|
||||
|
||||
// Auto-Connect für gepairte Bluetooth-Geräte
|
||||
ESP_LOGI(TAG, "Bluetooth bereit - Geräte können sich verbinden");
|
||||
bt_manager_set_discoverable(true);
|
||||
|
||||
ESP_LOGI(TAG, "=================================");
|
||||
ESP_LOGI(TAG, "System bereit!");
|
||||
ESP_LOGI(TAG, "=================================");
|
||||
|
||||
// Main Loop - Status-Reporting
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10000)); // Alle 10 Sekunden
|
||||
|
||||
// Status-Info ausgeben
|
||||
audio_source_t source = audio_router_get_active_source();
|
||||
const char* sources[] = {"Keine", "USB", "Bluetooth"};
|
||||
|
||||
ESP_LOGI(TAG, "Status: WiFi=%s, SIP=%s, Audio=%s",
|
||||
(wifi_manager_get_state() == WIFI_STATE_STA_CONNECTED) ? "Verbunden" : "Getrennt",
|
||||
(sip_client_get_reg_state() == SIP_REG_STATE_REGISTERED) ? "Registriert" : "Nicht registriert",
|
||||
sources[source]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,139 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// SIP-Registrierungsstatus
|
||||
typedef enum {
|
||||
SIP_REG_STATE_UNREGISTERED = 0,
|
||||
SIP_REG_STATE_REGISTERING,
|
||||
SIP_REG_STATE_REGISTERED,
|
||||
SIP_REG_STATE_FAILED
|
||||
} sip_reg_state_t;
|
||||
|
||||
// Anrufstatus
|
||||
typedef enum {
|
||||
SIP_CALL_STATE_IDLE = 0,
|
||||
SIP_CALL_STATE_INCOMING, // Eingehender Anruf
|
||||
SIP_CALL_STATE_OUTGOING, // Ausgehender Anruf
|
||||
SIP_CALL_STATE_RINGING, // Klingelt
|
||||
SIP_CALL_STATE_CONNECTED, // Verbunden
|
||||
SIP_CALL_STATE_HOLD, // Gehalten
|
||||
SIP_CALL_STATE_DISCONNECTING,
|
||||
SIP_CALL_STATE_DISCONNECTED
|
||||
} sip_call_state_t;
|
||||
|
||||
// Anrufinformationen
|
||||
typedef struct {
|
||||
char call_id[64];
|
||||
char remote_uri[128]; // SIP URI des Gegenübers
|
||||
char remote_name[64]; // Display Name
|
||||
char remote_number[32]; // Telefonnummer
|
||||
sip_call_state_t state;
|
||||
uint32_t duration_sec; // Anrufdauer in Sekunden
|
||||
bool is_incoming;
|
||||
} sip_call_info_t;
|
||||
|
||||
// RTP Audio Format
|
||||
typedef struct {
|
||||
uint32_t sample_rate; // Typisch 8000 oder 16000 Hz
|
||||
uint8_t payload_type; // 0=PCMU, 8=PCMA, etc.
|
||||
uint8_t channels;
|
||||
} sip_audio_format_t;
|
||||
|
||||
// Callbacks
|
||||
typedef void (*sip_reg_callback_t)(sip_reg_state_t state, const char* message);
|
||||
typedef void (*sip_call_callback_t)(const sip_call_info_t* call_info);
|
||||
typedef void (*sip_audio_callback_t)(const uint8_t* data, size_t len, const sip_audio_format_t* format);
|
||||
|
||||
/**
|
||||
* Initialisiert den SIP-Client
|
||||
*/
|
||||
esp_err_t sip_client_init(void);
|
||||
|
||||
/**
|
||||
* Deinitalisiert den SIP-Client
|
||||
*/
|
||||
esp_err_t sip_client_deinit(void);
|
||||
|
||||
/**
|
||||
* Registriert bei der TK-Anlage
|
||||
* Verwendet Konfiguration aus config_manager
|
||||
*/
|
||||
esp_err_t sip_client_register(void);
|
||||
|
||||
/**
|
||||
* Meldet sich von der TK-Anlage ab
|
||||
*/
|
||||
esp_err_t sip_client_unregister(void);
|
||||
|
||||
/**
|
||||
* Gibt den Registrierungsstatus zurück
|
||||
*/
|
||||
sip_reg_state_t sip_client_get_reg_state(void);
|
||||
|
||||
/**
|
||||
* Nimmt einen eingehenden Anruf an
|
||||
*/
|
||||
esp_err_t sip_client_answer(void);
|
||||
|
||||
/**
|
||||
* Lehnt einen eingehenden Anruf ab
|
||||
*/
|
||||
esp_err_t sip_client_reject(void);
|
||||
|
||||
/**
|
||||
* Beendet den aktuellen Anruf
|
||||
*/
|
||||
esp_err_t sip_client_hangup(void);
|
||||
|
||||
/**
|
||||
* Tätigt einen ausgehenden Anruf
|
||||
*/
|
||||
esp_err_t sip_client_call(const char* number);
|
||||
|
||||
/**
|
||||
* Sendet DTMF-Töne
|
||||
*/
|
||||
esp_err_t sip_client_send_dtmf(char digit);
|
||||
|
||||
/**
|
||||
* Setzt den Anruf auf Hold
|
||||
*/
|
||||
esp_err_t sip_client_hold(void);
|
||||
|
||||
/**
|
||||
* Holt den Anruf von Hold zurück
|
||||
*/
|
||||
esp_err_t sip_client_unhold(void);
|
||||
|
||||
/**
|
||||
* Gibt Informationen über den aktuellen Anruf zurück
|
||||
*/
|
||||
esp_err_t sip_client_get_call_info(sip_call_info_t* info);
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Anrufstatus zurück
|
||||
*/
|
||||
sip_call_state_t sip_client_get_call_state(void);
|
||||
|
||||
/**
|
||||
* Sendet Audio-Daten (RTP)
|
||||
*/
|
||||
esp_err_t sip_client_send_audio(const uint8_t* data, size_t len);
|
||||
|
||||
/**
|
||||
* Registriert Callbacks
|
||||
*/
|
||||
void sip_client_register_reg_callback(sip_reg_callback_t callback);
|
||||
void sip_client_register_call_callback(sip_call_callback_t callback);
|
||||
void sip_client_register_audio_callback(sip_audio_callback_t callback);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
/**
|
||||
* SIP Parser - Einfacher SIP Message Parser
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include "sip_parser.h"
|
||||
|
||||
// Hilfsfunktion: Zeile aus Buffer extrahieren
|
||||
static int get_line(const char* buf, size_t buf_len, char* line, size_t line_len)
|
||||
{
|
||||
size_t i = 0;
|
||||
while (i < buf_len && i < line_len - 1) {
|
||||
if (buf[i] == '\r' || buf[i] == '\n') {
|
||||
line[i] = '\0';
|
||||
// Skip CRLF
|
||||
if (buf[i] == '\r' && i + 1 < buf_len && buf[i + 1] == '\n') {
|
||||
return i + 2;
|
||||
}
|
||||
return i + 1;
|
||||
}
|
||||
line[i] = buf[i];
|
||||
i++;
|
||||
}
|
||||
line[i] = '\0';
|
||||
return i;
|
||||
}
|
||||
|
||||
// Hilfsfunktion: Header-Wert extrahieren
|
||||
static int get_header_value(const char* line, const char* header_name, char* value, size_t value_len)
|
||||
{
|
||||
size_t name_len = strlen(header_name);
|
||||
|
||||
// Case-insensitive Vergleich
|
||||
if (strncasecmp(line, header_name, name_len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Skip Header Name und ':'
|
||||
const char* p = line + name_len;
|
||||
while (*p && (*p == ':' || *p == ' ' || *p == '\t')) {
|
||||
p++;
|
||||
}
|
||||
|
||||
strncpy(value, p, value_len - 1);
|
||||
value[value_len - 1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sip_parse_message(const char* data, size_t len, sip_message_t* msg)
|
||||
{
|
||||
if (!data || !msg || len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(msg, 0, sizeof(sip_message_t));
|
||||
|
||||
const char* p = data;
|
||||
size_t remaining = len;
|
||||
char line[512];
|
||||
|
||||
// Erste Zeile parsen (Request-Line oder Status-Line)
|
||||
int line_len = get_line(p, remaining, line, sizeof(line));
|
||||
if (line_len <= 0) return -1;
|
||||
|
||||
p += line_len;
|
||||
remaining -= line_len;
|
||||
|
||||
// Status-Line: "SIP/2.0 200 OK"
|
||||
if (strncmp(line, "SIP/2.0 ", 8) == 0) {
|
||||
msg->is_request = false;
|
||||
msg->status_code = atoi(line + 8);
|
||||
|
||||
// Reason Phrase
|
||||
const char* reason = strchr(line + 8, ' ');
|
||||
if (reason) {
|
||||
strncpy(msg->reason_phrase, reason + 1, sizeof(msg->reason_phrase) - 1);
|
||||
}
|
||||
}
|
||||
// Request-Line: "INVITE sip:user@host SIP/2.0"
|
||||
else {
|
||||
msg->is_request = true;
|
||||
|
||||
// Method
|
||||
if (strncmp(line, "INVITE ", 7) == 0) {
|
||||
msg->method = SIP_METHOD_INVITE;
|
||||
} else if (strncmp(line, "ACK ", 4) == 0) {
|
||||
msg->method = SIP_METHOD_ACK;
|
||||
} else if (strncmp(line, "BYE ", 4) == 0) {
|
||||
msg->method = SIP_METHOD_BYE;
|
||||
} else if (strncmp(line, "CANCEL ", 7) == 0) {
|
||||
msg->method = SIP_METHOD_CANCEL;
|
||||
} else if (strncmp(line, "REGISTER ", 9) == 0) {
|
||||
msg->method = SIP_METHOD_REGISTER;
|
||||
} else if (strncmp(line, "OPTIONS ", 8) == 0) {
|
||||
msg->method = SIP_METHOD_OPTIONS;
|
||||
} else if (strncmp(line, "INFO ", 5) == 0) {
|
||||
msg->method = SIP_METHOD_INFO;
|
||||
} else {
|
||||
msg->method = SIP_METHOD_UNKNOWN;
|
||||
}
|
||||
|
||||
// Request URI
|
||||
char* uri_start = strchr(line, ' ');
|
||||
if (uri_start) {
|
||||
uri_start++;
|
||||
char* uri_end = strrchr(uri_start, ' ');
|
||||
if (uri_end) {
|
||||
size_t uri_len = uri_end - uri_start;
|
||||
if (uri_len < sizeof(msg->request_uri)) {
|
||||
strncpy(msg->request_uri, uri_start, uri_len);
|
||||
msg->request_uri[uri_len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Headers parsen
|
||||
while (remaining > 0) {
|
||||
line_len = get_line(p, remaining, line, sizeof(line));
|
||||
if (line_len <= 0) break;
|
||||
|
||||
p += line_len;
|
||||
remaining -= line_len;
|
||||
|
||||
// Leere Zeile = Ende der Header, Body beginnt
|
||||
if (line[0] == '\0') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Header parsen
|
||||
if (get_header_value(line, "Via", msg->via, sizeof(msg->via)) == 0) continue;
|
||||
if (get_header_value(line, "v", msg->via, sizeof(msg->via)) == 0) continue;
|
||||
|
||||
if (get_header_value(line, "From", msg->from, sizeof(msg->from)) == 0) {
|
||||
sip_extract_tag(msg->from, msg->from_tag, sizeof(msg->from_tag));
|
||||
continue;
|
||||
}
|
||||
if (get_header_value(line, "f", msg->from, sizeof(msg->from)) == 0) {
|
||||
sip_extract_tag(msg->from, msg->from_tag, sizeof(msg->from_tag));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_header_value(line, "To", msg->to, sizeof(msg->to)) == 0) {
|
||||
sip_extract_tag(msg->to, msg->to_tag, sizeof(msg->to_tag));
|
||||
continue;
|
||||
}
|
||||
if (get_header_value(line, "t", msg->to, sizeof(msg->to)) == 0) {
|
||||
sip_extract_tag(msg->to, msg->to_tag, sizeof(msg->to_tag));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_header_value(line, "Call-ID", msg->call_id, sizeof(msg->call_id)) == 0) continue;
|
||||
if (get_header_value(line, "i", msg->call_id, sizeof(msg->call_id)) == 0) continue;
|
||||
|
||||
if (strncasecmp(line, "CSeq:", 5) == 0 || strncasecmp(line, "CSeq :", 6) == 0) {
|
||||
const char* cseq_val = strchr(line, ':');
|
||||
if (cseq_val) {
|
||||
cseq_val++;
|
||||
while (*cseq_val == ' ') cseq_val++;
|
||||
msg->cseq = atoi(cseq_val);
|
||||
|
||||
// Method aus CSeq
|
||||
const char* method = strchr(cseq_val, ' ');
|
||||
if (method) {
|
||||
method++;
|
||||
if (strncmp(method, "INVITE", 6) == 0) msg->cseq_method = SIP_METHOD_INVITE;
|
||||
else if (strncmp(method, "REGISTER", 8) == 0) msg->cseq_method = SIP_METHOD_REGISTER;
|
||||
else if (strncmp(method, "BYE", 3) == 0) msg->cseq_method = SIP_METHOD_BYE;
|
||||
else if (strncmp(method, "ACK", 3) == 0) msg->cseq_method = SIP_METHOD_ACK;
|
||||
else if (strncmp(method, "CANCEL", 6) == 0) msg->cseq_method = SIP_METHOD_CANCEL;
|
||||
else if (strncmp(method, "OPTIONS", 7) == 0) msg->cseq_method = SIP_METHOD_OPTIONS;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_header_value(line, "Contact", msg->contact, sizeof(msg->contact)) == 0) continue;
|
||||
if (get_header_value(line, "m", msg->contact, sizeof(msg->contact)) == 0) continue;
|
||||
|
||||
if (strncasecmp(line, "Content-Length:", 15) == 0) {
|
||||
const char* cl = strchr(line, ':');
|
||||
if (cl) msg->content_length = atoi(cl + 1);
|
||||
continue;
|
||||
}
|
||||
if (strncasecmp(line, "l:", 2) == 0) {
|
||||
msg->content_length = atoi(line + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (get_header_value(line, "Content-Type", msg->content_type, sizeof(msg->content_type)) == 0) continue;
|
||||
if (get_header_value(line, "c", msg->content_type, sizeof(msg->content_type)) == 0) continue;
|
||||
|
||||
if (get_header_value(line, "WWW-Authenticate", msg->www_authenticate, sizeof(msg->www_authenticate)) == 0) continue;
|
||||
if (get_header_value(line, "Proxy-Authenticate", msg->proxy_authenticate, sizeof(msg->proxy_authenticate)) == 0) continue;
|
||||
}
|
||||
|
||||
// Body (SDP)
|
||||
if (remaining > 0 && msg->content_length > 0) {
|
||||
size_t body_len = remaining < sizeof(msg->sdp_body) - 1 ? remaining : sizeof(msg->sdp_body) - 1;
|
||||
if (body_len > (size_t)msg->content_length) {
|
||||
body_len = msg->content_length;
|
||||
}
|
||||
strncpy(msg->sdp_body, p, body_len);
|
||||
msg->sdp_body[body_len] = '\0';
|
||||
|
||||
if (strstr(msg->content_type, "application/sdp") != NULL) {
|
||||
msg->has_sdp = true;
|
||||
sip_parse_sdp(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sip_extract_uri(const char* header, char* uri, size_t uri_len)
|
||||
{
|
||||
// Suche <uri> oder sip:uri
|
||||
const char* start = strchr(header, '<');
|
||||
if (start) {
|
||||
start++;
|
||||
const char* end = strchr(start, '>');
|
||||
if (end) {
|
||||
size_t len = end - start;
|
||||
if (len < uri_len) {
|
||||
strncpy(uri, start, len);
|
||||
uri[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: sip: suchen
|
||||
start = strstr(header, "sip:");
|
||||
if (!start) start = strstr(header, "sips:");
|
||||
if (start) {
|
||||
const char* end = start;
|
||||
while (*end && *end != '>' && *end != ';' && *end != ' ') {
|
||||
end++;
|
||||
}
|
||||
size_t len = end - start;
|
||||
if (len < uri_len) {
|
||||
strncpy(uri, start, len);
|
||||
uri[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sip_extract_tag(const char* header, char* tag, size_t tag_len)
|
||||
{
|
||||
const char* tag_start = strstr(header, "tag=");
|
||||
if (!tag_start) return -1;
|
||||
|
||||
tag_start += 4;
|
||||
const char* tag_end = tag_start;
|
||||
while (*tag_end && *tag_end != ';' && *tag_end != '>' && *tag_end != ' ') {
|
||||
tag_end++;
|
||||
}
|
||||
|
||||
size_t len = tag_end - tag_start;
|
||||
if (len < tag_len) {
|
||||
strncpy(tag, tag_start, len);
|
||||
tag[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sip_extract_display_name(const char* header, char* name, size_t name_len)
|
||||
{
|
||||
// "Display Name" <sip:user@host>
|
||||
const char* quote_start = strchr(header, '"');
|
||||
if (quote_start) {
|
||||
quote_start++;
|
||||
const char* quote_end = strchr(quote_start, '"');
|
||||
if (quote_end) {
|
||||
size_t len = quote_end - quote_start;
|
||||
if (len < name_len) {
|
||||
strncpy(name, quote_start, len);
|
||||
name[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display Name <sip:user@host>
|
||||
const char* bracket = strchr(header, '<');
|
||||
if (bracket && bracket > header) {
|
||||
const char* start = header;
|
||||
while (*start == ' ') start++;
|
||||
size_t len = bracket - start;
|
||||
while (len > 0 && start[len - 1] == ' ') len--;
|
||||
if (len > 0 && len < name_len) {
|
||||
strncpy(name, start, len);
|
||||
name[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sip_parse_sdp(sip_message_t* msg)
|
||||
{
|
||||
if (!msg->has_sdp || msg->sdp_body[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// RTP Port und IP aus SDP extrahieren
|
||||
// m=audio <port> RTP/AVP <payload_types>
|
||||
// c=IN IP4 <ip>
|
||||
|
||||
const char* m_line = strstr(msg->sdp_body, "m=audio ");
|
||||
if (m_line) {
|
||||
m_line += 8;
|
||||
msg->rtp_port = (uint16_t)atoi(m_line);
|
||||
|
||||
// Payload Type
|
||||
const char* pt = strstr(m_line, " RTP/AVP ");
|
||||
if (pt) {
|
||||
pt += 9;
|
||||
msg->rtp_payload_type = (uint8_t)atoi(pt);
|
||||
}
|
||||
}
|
||||
|
||||
const char* c_line = strstr(msg->sdp_body, "c=IN IP4 ");
|
||||
if (c_line) {
|
||||
c_line += 9;
|
||||
const char* end = c_line;
|
||||
while (*end && *end != '\r' && *end != '\n') end++;
|
||||
size_t len = end - c_line;
|
||||
if (len < sizeof(msg->rtp_ip)) {
|
||||
strncpy(msg->rtp_ip, c_line, len);
|
||||
msg->rtp_ip[len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// SIP Methods
|
||||
typedef enum {
|
||||
SIP_METHOD_UNKNOWN = 0,
|
||||
SIP_METHOD_INVITE,
|
||||
SIP_METHOD_ACK,
|
||||
SIP_METHOD_BYE,
|
||||
SIP_METHOD_CANCEL,
|
||||
SIP_METHOD_REGISTER,
|
||||
SIP_METHOD_OPTIONS,
|
||||
SIP_METHOD_INFO,
|
||||
SIP_METHOD_UPDATE,
|
||||
SIP_METHOD_PRACK
|
||||
} sip_method_t;
|
||||
|
||||
// Geparste SIP-Nachricht
|
||||
typedef struct {
|
||||
bool is_request; // true = Request, false = Response
|
||||
|
||||
// Request Line
|
||||
sip_method_t method;
|
||||
char request_uri[128];
|
||||
|
||||
// Status Line (Response)
|
||||
int status_code;
|
||||
char reason_phrase[64];
|
||||
|
||||
// Headers
|
||||
char via[256];
|
||||
char from[256];
|
||||
char from_tag[64];
|
||||
char to[256];
|
||||
char to_tag[64];
|
||||
char call_id[128];
|
||||
int cseq;
|
||||
sip_method_t cseq_method;
|
||||
char contact[256];
|
||||
int content_length;
|
||||
char content_type[64];
|
||||
|
||||
// Authorization
|
||||
char www_authenticate[512];
|
||||
char proxy_authenticate[512];
|
||||
|
||||
// SDP Body
|
||||
char sdp_body[2048];
|
||||
bool has_sdp;
|
||||
|
||||
// RTP Info aus SDP
|
||||
char rtp_ip[32];
|
||||
uint16_t rtp_port;
|
||||
uint8_t rtp_payload_type;
|
||||
|
||||
} sip_message_t;
|
||||
|
||||
/**
|
||||
* Parst eine SIP-Nachricht
|
||||
*/
|
||||
int sip_parse_message(const char* data, size_t len, sip_message_t* msg);
|
||||
|
||||
/**
|
||||
* Extrahiert URI aus Header
|
||||
*/
|
||||
int sip_extract_uri(const char* header, char* uri, size_t uri_len);
|
||||
|
||||
/**
|
||||
* Extrahiert Tag aus Header
|
||||
*/
|
||||
int sip_extract_tag(const char* header, char* tag, size_t tag_len);
|
||||
|
||||
/**
|
||||
* Extrahiert Display-Name aus Header
|
||||
*/
|
||||
int sip_extract_display_name(const char* header, char* name, size_t name_len);
|
||||
|
||||
/**
|
||||
* Parst SDP und extrahiert RTP-Info
|
||||
*/
|
||||
int sip_parse_sdp(sip_message_t* msg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,356 @@
|
|||
/**
|
||||
* USB Audio Host - USB Headset Unterstützung
|
||||
*
|
||||
* Verwendet ESP32-S3 USB OTG im Host-Modus für USB Audio Class Geräte
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "usb_audio_host.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/ringbuf.h"
|
||||
#include "usb/usb_host.h"
|
||||
|
||||
static const char* TAG = "USB_AUDIO";
|
||||
|
||||
// USB Audio Class Definitionen
|
||||
#define USB_CLASS_AUDIO 0x01
|
||||
#define USB_SUBCLASS_AUDIOCONTROL 0x01
|
||||
#define USB_SUBCLASS_AUDIOSTREAMING 0x02
|
||||
#define USB_CLASS_HID 0x03
|
||||
|
||||
// State
|
||||
static bool s_initialized = false;
|
||||
static bool s_host_installed = false;
|
||||
static usb_audio_state_t s_state = USB_AUDIO_STATE_NOT_CONNECTED;
|
||||
static usb_audio_info_t s_device_info;
|
||||
static usb_host_client_handle_t s_client_handle = NULL;
|
||||
static usb_device_handle_t s_device_handle = NULL;
|
||||
static TaskHandle_t s_usb_task_handle = NULL;
|
||||
|
||||
// Audio Buffers
|
||||
#define AUDIO_RINGBUF_SIZE (16 * 1024)
|
||||
static RingbufHandle_t s_audio_in_ringbuf = NULL; // Vom USB Mic
|
||||
static RingbufHandle_t s_audio_out_ringbuf = NULL; // Zum USB Speaker
|
||||
|
||||
// Callbacks
|
||||
static usb_audio_state_callback_t s_state_callback = NULL;
|
||||
static usb_audio_data_callback_t s_data_callback = NULL;
|
||||
static usb_button_callback_t s_button_callback = NULL;
|
||||
|
||||
static void notify_state_change(usb_audio_state_t new_state)
|
||||
{
|
||||
s_state = new_state;
|
||||
if (s_state_callback) {
|
||||
s_state_callback(new_state);
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_host_client_event_callback(const usb_host_client_event_msg_t *event_msg, void *arg)
|
||||
{
|
||||
switch (event_msg->event) {
|
||||
case USB_HOST_CLIENT_EVENT_NEW_DEV:
|
||||
ESP_LOGI(TAG, "Neues USB-Gerät gefunden: Adresse %d", event_msg->new_dev.address);
|
||||
|
||||
// Gerät öffnen
|
||||
if (usb_host_device_open(s_client_handle, event_msg->new_dev.address,
|
||||
&s_device_handle) == ESP_OK) {
|
||||
// Device Descriptor holen
|
||||
const usb_device_desc_t *dev_desc;
|
||||
if (usb_host_get_device_descriptor(s_device_handle, &dev_desc) == ESP_OK) {
|
||||
s_device_info.vendor_id = dev_desc->idVendor;
|
||||
s_device_info.product_id = dev_desc->idProduct;
|
||||
|
||||
ESP_LOGI(TAG, "USB Gerät: VID=0x%04X PID=0x%04X",
|
||||
dev_desc->idVendor, dev_desc->idProduct);
|
||||
|
||||
// Configuration Descriptor analysieren
|
||||
const usb_config_desc_t *config_desc;
|
||||
if (usb_host_get_active_config_descriptor(s_device_handle, &config_desc) == ESP_OK) {
|
||||
// Interfaces durchgehen
|
||||
int offset = 0;
|
||||
const usb_intf_desc_t *intf_desc;
|
||||
|
||||
while ((intf_desc = usb_parse_interface_descriptor(
|
||||
config_desc, offset, 0, &offset)) != NULL) {
|
||||
|
||||
if (intf_desc->bInterfaceClass == USB_CLASS_AUDIO) {
|
||||
if (intf_desc->bInterfaceSubClass == USB_SUBCLASS_AUDIOSTREAMING) {
|
||||
ESP_LOGI(TAG, "Audio Streaming Interface gefunden");
|
||||
s_device_info.has_speaker = true;
|
||||
s_device_info.has_microphone = true;
|
||||
}
|
||||
} else if (intf_desc->bInterfaceClass == USB_CLASS_HID) {
|
||||
ESP_LOGI(TAG, "HID Interface gefunden (Tasten)");
|
||||
s_device_info.has_hid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_device_info.has_speaker || s_device_info.has_microphone) {
|
||||
notify_state_change(USB_AUDIO_STATE_CONNECTED);
|
||||
|
||||
// Standard Audio-Format
|
||||
s_device_info.sample_rate = 16000;
|
||||
s_device_info.channels = 1;
|
||||
s_device_info.bit_depth = 16;
|
||||
|
||||
ESP_LOGI(TAG, "USB Audio Headset erkannt");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case USB_HOST_CLIENT_EVENT_DEV_GONE:
|
||||
ESP_LOGI(TAG, "USB-Gerät entfernt");
|
||||
|
||||
if (s_device_handle) {
|
||||
usb_host_device_close(s_client_handle, s_device_handle);
|
||||
s_device_handle = NULL;
|
||||
}
|
||||
|
||||
memset(&s_device_info, 0, sizeof(s_device_info));
|
||||
notify_state_change(USB_AUDIO_STATE_NOT_CONNECTED);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_host_task(void *arg)
|
||||
{
|
||||
ESP_LOGI(TAG, "USB Host Task gestartet");
|
||||
|
||||
while (s_initialized) {
|
||||
// USB Host Events verarbeiten
|
||||
usb_host_lib_handle_events(pdMS_TO_TICKS(100), NULL);
|
||||
|
||||
// Client Events verarbeiten
|
||||
if (s_client_handle) {
|
||||
usb_host_client_handle_events(s_client_handle, pdMS_TO_TICKS(100));
|
||||
}
|
||||
|
||||
// Wenn verbunden: Audio-Daten verarbeiten
|
||||
if (s_state == USB_AUDIO_STATE_STREAMING && s_audio_in_ringbuf) {
|
||||
// Audio vom USB lesen und Callback aufrufen
|
||||
size_t item_size;
|
||||
uint8_t* data = xRingbufferReceive(s_audio_in_ringbuf, &item_size, 0);
|
||||
if (data && item_size > 0) {
|
||||
if (s_data_callback) {
|
||||
s_data_callback(data, item_size);
|
||||
}
|
||||
vRingbufferReturnItem(s_audio_in_ringbuf, data);
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "USB Host Task beendet");
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initialisiere USB Audio Host");
|
||||
|
||||
// Audio Buffers erstellen
|
||||
s_audio_in_ringbuf = xRingbufferCreate(AUDIO_RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
|
||||
s_audio_out_ringbuf = xRingbufferCreate(AUDIO_RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
|
||||
|
||||
if (!s_audio_in_ringbuf || !s_audio_out_ringbuf) {
|
||||
ESP_LOGE(TAG, "Ringbuffer erstellen fehlgeschlagen");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// USB Host Library installieren
|
||||
usb_host_config_t host_config = {
|
||||
.skip_phy_setup = false,
|
||||
.intr_flags = ESP_INTR_FLAG_LEVEL1,
|
||||
};
|
||||
|
||||
esp_err_t ret = usb_host_install(&host_config);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "USB Host install failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
s_host_installed = true;
|
||||
|
||||
// Client registrieren
|
||||
usb_host_client_config_t client_config = {
|
||||
.is_synchronous = false,
|
||||
.max_num_event_msg = 5,
|
||||
.async = {
|
||||
.client_event_callback = usb_host_client_event_callback,
|
||||
.callback_arg = NULL,
|
||||
},
|
||||
};
|
||||
|
||||
ret = usb_host_client_register(&client_config, &s_client_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "USB Client register failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
s_initialized = true;
|
||||
|
||||
// USB Host Task starten
|
||||
xTaskCreate(usb_host_task, "usb_host", 4096, NULL, 5, &s_usb_task_handle);
|
||||
|
||||
ESP_LOGI(TAG, "USB Audio Host initialisiert");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_deinit(void)
|
||||
{
|
||||
if (!s_initialized) return ESP_OK;
|
||||
|
||||
ESP_LOGI(TAG, "Deinitalisiere USB Audio Host");
|
||||
|
||||
s_initialized = false;
|
||||
|
||||
// Warten auf Task-Ende
|
||||
if (s_usb_task_handle) {
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
|
||||
// Gerät schließen
|
||||
if (s_device_handle && s_client_handle) {
|
||||
usb_host_device_close(s_client_handle, s_device_handle);
|
||||
s_device_handle = NULL;
|
||||
}
|
||||
|
||||
// Client deregistrieren
|
||||
if (s_client_handle) {
|
||||
usb_host_client_deregister(s_client_handle);
|
||||
s_client_handle = NULL;
|
||||
}
|
||||
|
||||
// USB Host deinstallieren
|
||||
if (s_host_installed) {
|
||||
usb_host_uninstall();
|
||||
s_host_installed = false;
|
||||
}
|
||||
|
||||
// Buffers freigeben
|
||||
if (s_audio_in_ringbuf) {
|
||||
vRingbufferDelete(s_audio_in_ringbuf);
|
||||
s_audio_in_ringbuf = NULL;
|
||||
}
|
||||
if (s_audio_out_ringbuf) {
|
||||
vRingbufferDelete(s_audio_out_ringbuf);
|
||||
s_audio_out_ringbuf = NULL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
usb_audio_state_t usb_audio_host_get_state(void)
|
||||
{
|
||||
return s_state;
|
||||
}
|
||||
|
||||
bool usb_audio_host_is_connected(void)
|
||||
{
|
||||
return s_state >= USB_AUDIO_STATE_CONNECTED;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_get_info(usb_audio_info_t* info)
|
||||
{
|
||||
if (!info) return ESP_ERR_INVALID_ARG;
|
||||
if (s_state == USB_AUDIO_STATE_NOT_CONNECTED) return ESP_ERR_INVALID_STATE;
|
||||
|
||||
memcpy(info, &s_device_info, sizeof(usb_audio_info_t));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_start_stream(void)
|
||||
{
|
||||
if (s_state < USB_AUDIO_STATE_CONNECTED) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starte USB Audio Stream");
|
||||
|
||||
// TODO: Echte USB Audio Stream Konfiguration
|
||||
// Dies würde das Konfigurieren der Audio-Interfaces,
|
||||
// Setzen der Samplerate und Starten der Isochronen Transfers beinhalten
|
||||
|
||||
s_state = USB_AUDIO_STATE_STREAMING;
|
||||
notify_state_change(USB_AUDIO_STATE_STREAMING);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_stop_stream(void)
|
||||
{
|
||||
if (s_state != USB_AUDIO_STATE_STREAMING) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stoppe USB Audio Stream");
|
||||
|
||||
s_state = USB_AUDIO_STATE_CONNECTED;
|
||||
notify_state_change(USB_AUDIO_STATE_CONNECTED);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_send(const uint8_t* data, size_t len)
|
||||
{
|
||||
if (s_state != USB_AUDIO_STATE_STREAMING || !s_audio_out_ringbuf) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (xRingbufferSend(s_audio_out_ringbuf, data, len, 0) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "Audio buffer overflow");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_set_volume(uint8_t volume)
|
||||
{
|
||||
if (s_state < USB_AUDIO_STATE_CONNECTED) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Setze Lautstärke: %d%%", volume);
|
||||
|
||||
// TODO: USB Audio Class Volume Control implementieren
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t usb_audio_host_set_mute(bool mute)
|
||||
{
|
||||
if (s_state < USB_AUDIO_STATE_CONNECTED) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Mute: %s", mute ? "an" : "aus");
|
||||
|
||||
// TODO: USB Audio Class Mute Control implementieren
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void usb_audio_host_register_state_callback(usb_audio_state_callback_t callback)
|
||||
{
|
||||
s_state_callback = callback;
|
||||
}
|
||||
|
||||
void usb_audio_host_register_data_callback(usb_audio_data_callback_t callback)
|
||||
{
|
||||
s_data_callback = callback;
|
||||
}
|
||||
|
||||
void usb_audio_host_register_button_callback(usb_button_callback_t callback)
|
||||
{
|
||||
s_button_callback = callback;
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// USB Audio Status
|
||||
typedef enum {
|
||||
USB_AUDIO_STATE_NOT_CONNECTED = 0,
|
||||
USB_AUDIO_STATE_CONNECTED,
|
||||
USB_AUDIO_STATE_CONFIGURED,
|
||||
USB_AUDIO_STATE_STREAMING,
|
||||
USB_AUDIO_STATE_ERROR
|
||||
} usb_audio_state_t;
|
||||
|
||||
// USB Headset Button Events (HID)
|
||||
typedef enum {
|
||||
USB_BUTTON_NONE = 0,
|
||||
USB_BUTTON_ANSWER,
|
||||
USB_BUTTON_HANGUP,
|
||||
USB_BUTTON_MUTE,
|
||||
USB_BUTTON_VOLUME_UP,
|
||||
USB_BUTTON_VOLUME_DOWN
|
||||
} usb_button_event_t;
|
||||
|
||||
// USB Audio Info
|
||||
typedef struct {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
char product_name[64];
|
||||
char manufacturer[64];
|
||||
uint32_t sample_rate;
|
||||
uint8_t channels;
|
||||
uint8_t bit_depth;
|
||||
bool has_microphone;
|
||||
bool has_speaker;
|
||||
bool has_hid; // Hat HID-Tasten
|
||||
} usb_audio_info_t;
|
||||
|
||||
// Callbacks
|
||||
typedef void (*usb_audio_state_callback_t)(usb_audio_state_t state);
|
||||
typedef void (*usb_audio_data_callback_t)(const uint8_t* data, size_t len);
|
||||
typedef void (*usb_button_callback_t)(usb_button_event_t event);
|
||||
|
||||
/**
|
||||
* Initialisiert den USB Audio Host
|
||||
*/
|
||||
esp_err_t usb_audio_host_init(void);
|
||||
|
||||
/**
|
||||
* Deinitialisiert den USB Audio Host
|
||||
*/
|
||||
esp_err_t usb_audio_host_deinit(void);
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Status zurück
|
||||
*/
|
||||
usb_audio_state_t usb_audio_host_get_state(void);
|
||||
|
||||
/**
|
||||
* Prüft ob ein USB-Headset angeschlossen ist
|
||||
*/
|
||||
bool usb_audio_host_is_connected(void);
|
||||
|
||||
/**
|
||||
* Gibt Informationen über das angeschlossene Gerät zurück
|
||||
*/
|
||||
esp_err_t usb_audio_host_get_info(usb_audio_info_t* info);
|
||||
|
||||
/**
|
||||
* Startet Audio-Streaming
|
||||
*/
|
||||
esp_err_t usb_audio_host_start_stream(void);
|
||||
|
||||
/**
|
||||
* Stoppt Audio-Streaming
|
||||
*/
|
||||
esp_err_t usb_audio_host_stop_stream(void);
|
||||
|
||||
/**
|
||||
* Sendet Audio-Daten zum USB-Headset (Speaker)
|
||||
*/
|
||||
esp_err_t usb_audio_host_send(const uint8_t* data, size_t len);
|
||||
|
||||
/**
|
||||
* Setzt die Lautstärke (0-100)
|
||||
*/
|
||||
esp_err_t usb_audio_host_set_volume(uint8_t volume);
|
||||
|
||||
/**
|
||||
* Setzt Mute
|
||||
*/
|
||||
esp_err_t usb_audio_host_set_mute(bool mute);
|
||||
|
||||
/**
|
||||
* Registriert Callbacks
|
||||
*/
|
||||
void usb_audio_host_register_state_callback(usb_audio_state_callback_t callback);
|
||||
void usb_audio_host_register_data_callback(usb_audio_data_callback_t callback);
|
||||
void usb_audio_host_register_button_callback(usb_button_callback_t callback);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
// ESP32 SIP Phone - Web Interface
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// State
|
||||
let currentTab = 'status';
|
||||
let statusUpdateInterval = null;
|
||||
|
||||
// DOM Elements
|
||||
const elements = {
|
||||
// Status bar
|
||||
wifiStatus: document.getElementById('wifi-status'),
|
||||
sipStatus: document.getElementById('sip-status'),
|
||||
audioStatus: document.getElementById('audio-status'),
|
||||
|
||||
// Call info
|
||||
callState: document.getElementById('call-state'),
|
||||
callRemote: document.getElementById('call-remote'),
|
||||
callDuration: document.getElementById('call-duration'),
|
||||
callButtons: document.getElementById('call-buttons'),
|
||||
btnAnswer: document.getElementById('btn-answer'),
|
||||
btnReject: document.getElementById('btn-reject'),
|
||||
btnHangup: document.getElementById('btn-hangup'),
|
||||
|
||||
// Audio
|
||||
audioSource: document.getElementById('audio-source'),
|
||||
usbConnected: document.getElementById('usb-connected'),
|
||||
btConnected: document.getElementById('bt-connected'),
|
||||
volumeSlider: document.getElementById('volume-slider'),
|
||||
volumeValue: document.getElementById('volume-value'),
|
||||
muteCheckbox: document.getElementById('mute-checkbox'),
|
||||
|
||||
// WiFi
|
||||
wifiForm: document.getElementById('wifi-form'),
|
||||
wifiSsid: document.getElementById('wifi-ssid'),
|
||||
wifiPassword: document.getElementById('wifi-password'),
|
||||
btnScanWifi: document.getElementById('btn-scan-wifi'),
|
||||
wifiScanResults: document.getElementById('wifi-scan-results'),
|
||||
staticIpConfig: document.getElementById('static-ip-config'),
|
||||
|
||||
// SIP
|
||||
sipForm: document.getElementById('sip-form'),
|
||||
sipServer: document.getElementById('sip-server'),
|
||||
sipPort: document.getElementById('sip-port'),
|
||||
sipUsername: document.getElementById('sip-username'),
|
||||
sipPassword: document.getElementById('sip-password'),
|
||||
sipDisplayName: document.getElementById('sip-display-name'),
|
||||
|
||||
// Bluetooth
|
||||
btnScanBt: document.getElementById('btn-scan-bt'),
|
||||
btPairedDevices: document.getElementById('bt-paired-devices'),
|
||||
btFoundDevices: document.getElementById('bt-found-devices'),
|
||||
|
||||
// System
|
||||
btnReboot: document.getElementById('btn-reboot'),
|
||||
btnFactoryReset: document.getElementById('btn-factory-reset')
|
||||
};
|
||||
|
||||
// API Functions
|
||||
async function apiGet(endpoint) {
|
||||
try {
|
||||
const response = await fetch('/api/' + endpoint);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiPost(endpoint, data = {}) {
|
||||
try {
|
||||
const response = await fetch('/api/' + endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Status Updates
|
||||
async function updateStatus() {
|
||||
const status = await apiGet('status');
|
||||
if (!status) return;
|
||||
|
||||
// WiFi
|
||||
if (status.wifi) {
|
||||
const wifiState = status.wifi.state === 'connected' ? 'Verbunden' :
|
||||
status.wifi.state === 'hotspot' ? 'Hotspot' : 'Getrennt';
|
||||
elements.wifiStatus.textContent = 'WiFi: ' + wifiState;
|
||||
elements.wifiStatus.className = 'status-item ' +
|
||||
(status.wifi.state === 'connected' ? 'status-connected' : 'status-disconnected');
|
||||
}
|
||||
|
||||
// SIP
|
||||
if (status.sip) {
|
||||
const sipState = status.sip.state === 'registered' ? 'Registriert' :
|
||||
status.sip.state === 'registering' ? 'Verbinde...' : 'Nicht registriert';
|
||||
elements.sipStatus.textContent = 'SIP: ' + sipState;
|
||||
elements.sipStatus.className = 'status-item ' +
|
||||
(status.sip.state === 'registered' ? 'status-connected' :
|
||||
status.sip.state === 'registering' ? 'status-pending' : 'status-disconnected');
|
||||
}
|
||||
|
||||
// Audio
|
||||
if (status.audio) {
|
||||
const source = status.audio.source === 'usb' ? 'USB' :
|
||||
status.audio.source === 'bluetooth' ? 'Bluetooth' : 'Keine';
|
||||
elements.audioStatus.textContent = 'Audio: ' + source;
|
||||
elements.audioSource.textContent = source;
|
||||
elements.usbConnected.textContent = status.audio.usb_connected ? 'Verbunden' : 'Nicht verbunden';
|
||||
elements.btConnected.textContent = status.audio.bt_connected ? 'Verbunden' : 'Nicht verbunden';
|
||||
elements.volumeSlider.value = status.audio.volume;
|
||||
elements.volumeValue.textContent = status.audio.volume + '%';
|
||||
elements.muteCheckbox.checked = status.audio.muted;
|
||||
}
|
||||
|
||||
// Call
|
||||
if (status.call) {
|
||||
updateCallUI(status.call);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCallUI(call) {
|
||||
const states = {
|
||||
'idle': 'Kein aktiver Anruf',
|
||||
'incoming': 'Eingehender Anruf',
|
||||
'outgoing': 'Ausgehender Anruf',
|
||||
'ringing': 'Klingelt...',
|
||||
'connected': 'Verbunden'
|
||||
};
|
||||
|
||||
elements.callState.textContent = states[call.state] || call.state;
|
||||
|
||||
if (call.state !== 'idle') {
|
||||
elements.callRemote.textContent = (call.name || '') + ' ' + (call.remote || '');
|
||||
if (call.state === 'connected' && call.duration !== undefined) {
|
||||
const mins = Math.floor(call.duration / 60);
|
||||
const secs = call.duration % 60;
|
||||
elements.callDuration.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
|
||||
} else {
|
||||
elements.callDuration.textContent = '';
|
||||
}
|
||||
elements.callButtons.classList.remove('hidden');
|
||||
|
||||
// Show appropriate buttons
|
||||
elements.btnAnswer.classList.toggle('hidden', call.state !== 'incoming');
|
||||
elements.btnReject.classList.toggle('hidden', call.state !== 'incoming');
|
||||
elements.btnHangup.classList.toggle('hidden', call.state === 'idle' || call.state === 'incoming');
|
||||
} else {
|
||||
elements.callRemote.textContent = '';
|
||||
elements.callDuration.textContent = '';
|
||||
elements.callButtons.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Tab Navigation
|
||||
function setupTabs() {
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tab = btn.dataset.tab;
|
||||
if (tab === currentTab) return;
|
||||
|
||||
// Update nav
|
||||
document.querySelector('.nav-btn.active').classList.remove('active');
|
||||
btn.classList.add('active');
|
||||
|
||||
// Update content
|
||||
document.querySelector('.tab-content.active').classList.remove('active');
|
||||
document.getElementById('tab-' + tab).classList.add('active');
|
||||
|
||||
currentTab = tab;
|
||||
|
||||
// Load tab-specific data
|
||||
if (tab === 'wifi') loadWifiConfig();
|
||||
if (tab === 'sip') loadSipConfig();
|
||||
if (tab === 'bluetooth') loadBluetoothDevices();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// WiFi
|
||||
async function loadWifiConfig() {
|
||||
const config = await apiGet('wifi/config');
|
||||
if (!config) return;
|
||||
|
||||
elements.wifiSsid.value = config.ssid || '';
|
||||
elements.wifiPassword.value = '';
|
||||
|
||||
const ipMode = config.ip_mode || 'dhcp';
|
||||
document.querySelector('input[name="ip-mode"][value="' + ipMode + '"]').checked = true;
|
||||
elements.staticIpConfig.classList.toggle('hidden', ipMode !== 'static');
|
||||
|
||||
if (ipMode === 'static') {
|
||||
document.getElementById('static-ip').value = config.static_ip || '';
|
||||
document.getElementById('gateway').value = config.gateway || '';
|
||||
document.getElementById('netmask').value = config.netmask || '';
|
||||
document.getElementById('dns').value = config.dns || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function scanWifi() {
|
||||
elements.btnScanWifi.disabled = true;
|
||||
elements.btnScanWifi.textContent = 'Scanne...';
|
||||
elements.wifiScanResults.classList.remove('hidden');
|
||||
elements.wifiScanResults.innerHTML = '<p class="no-devices">Scanne...</p>';
|
||||
|
||||
const networks = await apiGet('wifi/scan');
|
||||
|
||||
elements.btnScanWifi.disabled = false;
|
||||
elements.btnScanWifi.textContent = 'Scannen';
|
||||
|
||||
if (!networks || networks.length === 0) {
|
||||
elements.wifiScanResults.innerHTML = '<p class="no-devices">Keine Netzwerke gefunden</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
elements.wifiScanResults.innerHTML = networks.map(net =>
|
||||
'<div class="scan-item" data-ssid="' + escapeHtml(net.ssid) + '">' +
|
||||
'<span>' + escapeHtml(net.ssid) + (net.secure ? ' 🔒' : '') + '</span>' +
|
||||
'<span class="scan-rssi">' + net.rssi + ' dBm</span>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
|
||||
elements.wifiScanResults.querySelectorAll('.scan-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
elements.wifiSsid.value = item.dataset.ssid;
|
||||
elements.wifiScanResults.classList.add('hidden');
|
||||
elements.wifiPassword.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveWifiConfig(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const ipMode = document.querySelector('input[name="ip-mode"]:checked').value;
|
||||
const data = {
|
||||
ssid: elements.wifiSsid.value,
|
||||
password: elements.wifiPassword.value,
|
||||
ip_mode: ipMode
|
||||
};
|
||||
|
||||
if (ipMode === 'static') {
|
||||
data.static_ip = document.getElementById('static-ip').value;
|
||||
data.gateway = document.getElementById('gateway').value;
|
||||
data.netmask = document.getElementById('netmask').value;
|
||||
data.dns = document.getElementById('dns').value;
|
||||
}
|
||||
|
||||
const result = await apiPost('wifi/config', data);
|
||||
if (result && result.success) {
|
||||
alert('WiFi-Konfiguration gespeichert. Verbinde...');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
// SIP
|
||||
async function loadSipConfig() {
|
||||
const config = await apiGet('sip/config');
|
||||
if (!config) return;
|
||||
|
||||
elements.sipServer.value = config.server || '';
|
||||
elements.sipPort.value = config.port || 5060;
|
||||
elements.sipUsername.value = config.username || '';
|
||||
elements.sipPassword.value = '';
|
||||
elements.sipDisplayName.value = config.display_name || '';
|
||||
}
|
||||
|
||||
async function saveSipConfig(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
server: elements.sipServer.value,
|
||||
port: parseInt(elements.sipPort.value) || 5060,
|
||||
username: elements.sipUsername.value,
|
||||
password: elements.sipPassword.value,
|
||||
display_name: elements.sipDisplayName.value
|
||||
};
|
||||
|
||||
const result = await apiPost('sip/config', data);
|
||||
if (result && result.success) {
|
||||
alert('SIP-Konfiguration gespeichert. Registriere...');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth
|
||||
async function loadBluetoothDevices() {
|
||||
const devices = await apiGet('bluetooth/devices');
|
||||
|
||||
if (!devices || devices.length === 0) {
|
||||
elements.btPairedDevices.innerHTML = '<p class="no-devices">Keine Gerate gepaart</p>';
|
||||
} else {
|
||||
elements.btPairedDevices.innerHTML = devices.map(dev =>
|
||||
'<div class="device-item">' +
|
||||
'<div class="device-info">' +
|
||||
'<div class="device-name">' + escapeHtml(dev.name || 'Unbekannt') + '</div>' +
|
||||
'<div class="device-address">' + escapeHtml(dev.address) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="device-actions">' +
|
||||
'<button class="btn btn-secondary btn-connect" data-address="' + escapeHtml(dev.address) + '">Verbinden</button>' +
|
||||
'<button class="btn btn-danger btn-unpair" data-address="' + escapeHtml(dev.address) + '">Entfernen</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
|
||||
elements.btPairedDevices.querySelectorAll('.btn-connect').forEach(btn => {
|
||||
btn.addEventListener('click', () => connectBluetooth(btn.dataset.address));
|
||||
});
|
||||
|
||||
elements.btPairedDevices.querySelectorAll('.btn-unpair').forEach(btn => {
|
||||
btn.addEventListener('click', () => unpairBluetooth(btn.dataset.address));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function scanBluetooth() {
|
||||
elements.btnScanBt.disabled = true;
|
||||
elements.btnScanBt.textContent = 'Suche...';
|
||||
elements.btFoundDevices.innerHTML = '<p class="no-devices">Suche Gerate...</p>';
|
||||
|
||||
await apiPost('bluetooth/scan');
|
||||
|
||||
// Wait for scan to complete
|
||||
setTimeout(async () => {
|
||||
elements.btnScanBt.disabled = false;
|
||||
elements.btnScanBt.textContent = 'Gerate suchen';
|
||||
// In a full implementation, we'd poll for discovered devices
|
||||
elements.btFoundDevices.innerHTML = '<p class="no-devices">Suche abgeschlossen. Gerate werden automatisch gepaart wenn sie in den Pairing-Modus gehen.</p>';
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async function connectBluetooth(address) {
|
||||
const result = await apiPost('bluetooth/connect', { address });
|
||||
if (result && result.success) {
|
||||
alert('Verbinde...');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
async function unpairBluetooth(address) {
|
||||
if (!confirm('Gerat wirklich entfernen?')) return;
|
||||
|
||||
const result = await apiPost('bluetooth/unpair', { address });
|
||||
if (result && result.success) {
|
||||
loadBluetoothDevices();
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
// Call Actions
|
||||
async function answerCall() {
|
||||
await apiPost('call/answer');
|
||||
}
|
||||
|
||||
async function rejectCall() {
|
||||
await apiPost('call/reject');
|
||||
}
|
||||
|
||||
async function hangupCall() {
|
||||
await apiPost('call/hangup');
|
||||
}
|
||||
|
||||
// System
|
||||
async function reboot() {
|
||||
if (!confirm('System wirklich neustarten?')) return;
|
||||
await apiPost('system/reboot');
|
||||
alert('System startet neu...');
|
||||
}
|
||||
|
||||
async function factoryReset() {
|
||||
if (!confirm('ACHTUNG: Alle Einstellungen werden geloscht! Fortfahren?')) return;
|
||||
if (!confirm('Wirklich alle Einstellungen loschen?')) return;
|
||||
await apiPost('system/factory-reset');
|
||||
alert('Werksreset durchgefuhrt. System startet neu...');
|
||||
}
|
||||
|
||||
// Utilities
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
function setupEventListeners() {
|
||||
// Call buttons
|
||||
elements.btnAnswer.addEventListener('click', answerCall);
|
||||
elements.btnReject.addEventListener('click', rejectCall);
|
||||
elements.btnHangup.addEventListener('click', hangupCall);
|
||||
|
||||
// Volume
|
||||
elements.volumeSlider.addEventListener('input', () => {
|
||||
elements.volumeValue.textContent = elements.volumeSlider.value + '%';
|
||||
});
|
||||
|
||||
// WiFi
|
||||
elements.wifiForm.addEventListener('submit', saveWifiConfig);
|
||||
elements.btnScanWifi.addEventListener('click', scanWifi);
|
||||
|
||||
document.querySelectorAll('input[name="ip-mode"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
elements.staticIpConfig.classList.toggle('hidden', e.target.value !== 'static');
|
||||
});
|
||||
});
|
||||
|
||||
// SIP
|
||||
elements.sipForm.addEventListener('submit', saveSipConfig);
|
||||
|
||||
// Bluetooth
|
||||
elements.btnScanBt.addEventListener('click', scanBluetooth);
|
||||
|
||||
// System
|
||||
elements.btnReboot.addEventListener('click', reboot);
|
||||
elements.btnFactoryReset.addEventListener('click', factoryReset);
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
setupTabs();
|
||||
setupEventListeners();
|
||||
updateStatus();
|
||||
|
||||
// Update status every 2 seconds
|
||||
statusUpdateInterval = setInterval(updateStatus, 2000);
|
||||
}
|
||||
|
||||
// Start
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 SIP Phone</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>ESP32 SIP Phone</h1>
|
||||
<div id="status-bar">
|
||||
<span id="wifi-status" class="status-item">WiFi: --</span>
|
||||
<span id="sip-status" class="status-item">SIP: --</span>
|
||||
<span id="audio-status" class="status-item">Audio: --</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<button class="nav-btn active" data-tab="status">Status</button>
|
||||
<button class="nav-btn" data-tab="wifi">WLAN</button>
|
||||
<button class="nav-btn" data-tab="sip">SIP</button>
|
||||
<button class="nav-btn" data-tab="bluetooth">Bluetooth</button>
|
||||
<button class="nav-btn" data-tab="system">System</button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<!-- Status Tab -->
|
||||
<section id="tab-status" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2>Anrufstatus</h2>
|
||||
<div id="call-info">
|
||||
<p id="call-state">Kein aktiver Anruf</p>
|
||||
<p id="call-remote"></p>
|
||||
<p id="call-duration"></p>
|
||||
</div>
|
||||
<div id="call-buttons" class="button-group hidden">
|
||||
<button id="btn-answer" class="btn btn-success">Annehmen</button>
|
||||
<button id="btn-reject" class="btn btn-danger">Ablehnen</button>
|
||||
<button id="btn-hangup" class="btn btn-danger">Auflegen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Audio</h2>
|
||||
<p>Aktive Quelle: <strong id="audio-source">--</strong></p>
|
||||
<p>USB Headset: <span id="usb-connected">--</span></p>
|
||||
<p>Bluetooth Headset: <span id="bt-connected">--</span></p>
|
||||
<div class="volume-control">
|
||||
<label>Lautstarke:</label>
|
||||
<input type="range" id="volume-slider" min="0" max="100" value="80">
|
||||
<span id="volume-value">80%</span>
|
||||
</div>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="mute-checkbox"> Stummschalten
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- WiFi Tab -->
|
||||
<section id="tab-wifi" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>WLAN-Konfiguration</h2>
|
||||
<form id="wifi-form">
|
||||
<div class="form-group">
|
||||
<label for="wifi-ssid">SSID:</label>
|
||||
<div class="input-with-button">
|
||||
<input type="text" id="wifi-ssid" required>
|
||||
<button type="button" id="btn-scan-wifi" class="btn btn-secondary">Scannen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wifi-scan-results" class="scan-results hidden"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wifi-password">Passwort:</label>
|
||||
<input type="password" id="wifi-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>IP-Konfiguration:</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="ip-mode" value="dhcp" checked> DHCP</label>
|
||||
<label><input type="radio" name="ip-mode" value="static"> Statisch</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="static-ip-config" class="hidden">
|
||||
<div class="form-group">
|
||||
<label for="static-ip">IP-Adresse:</label>
|
||||
<input type="text" id="static-ip" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gateway">Gateway:</label>
|
||||
<input type="text" id="gateway" placeholder="192.168.1.1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="netmask">Netzmaske:</label>
|
||||
<input type="text" id="netmask" placeholder="255.255.255.0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dns">DNS:</label>
|
||||
<input type="text" id="dns" placeholder="8.8.8.8">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SIP Tab -->
|
||||
<section id="tab-sip" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>SIP-Konfiguration</h2>
|
||||
<form id="sip-form">
|
||||
<div class="form-group">
|
||||
<label for="sip-server">Server:</label>
|
||||
<input type="text" id="sip-server" placeholder="pbx.example.com" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-port">Port:</label>
|
||||
<input type="number" id="sip-port" value="5060">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-username">Benutzername:</label>
|
||||
<input type="text" id="sip-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-password">Passwort:</label>
|
||||
<input type="password" id="sip-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-display-name">Anzeigename:</label>
|
||||
<input type="text" id="sip-display-name" placeholder="Max Mustermann">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bluetooth Tab -->
|
||||
<section id="tab-bluetooth" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>Bluetooth Headsets</h2>
|
||||
<p class="hint">USB-Headsets haben Vorrang vor Bluetooth.</p>
|
||||
|
||||
<button id="btn-scan-bt" class="btn btn-secondary">Geräte suchen</button>
|
||||
|
||||
<h3>Gepaarte Geräte</h3>
|
||||
<div id="bt-paired-devices" class="device-list">
|
||||
<p class="no-devices">Keine Geräte gepaart</p>
|
||||
</div>
|
||||
|
||||
<h3>Gefundene Geräte</h3>
|
||||
<div id="bt-found-devices" class="device-list">
|
||||
<p class="no-devices">Starte Suche...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- System Tab -->
|
||||
<section id="tab-system" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>System</h2>
|
||||
<div class="button-group">
|
||||
<button id="btn-reboot" class="btn btn-secondary">Neustart</button>
|
||||
<button id="btn-factory-reset" class="btn btn-danger">Werksreset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Info</h2>
|
||||
<p>ESP32-S3 Bluetooth SIP Client</p>
|
||||
<p>Hotspot: ESP32-SIP-Phone</p>
|
||||
<p>Standard-IP: 192.168.4.1</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
/* ESP32 SIP Phone - Web Interface Styles */
|
||||
|
||||
:root {
|
||||
--primary: #2196F3;
|
||||
--primary-dark: #1976D2;
|
||||
--success: #4CAF50;
|
||||
--danger: #f44336;
|
||||
--warning: #FF9800;
|
||||
--bg: #f5f5f5;
|
||||
--card-bg: #ffffff;
|
||||
--text: #333333;
|
||||
--text-light: #666666;
|
||||
--border: #e0e0e0;
|
||||
--shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#status-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
background: var(--card-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-light);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
color: var(--primary);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-with-button input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Volume Control */
|
||||
.volume-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.volume-control input[type="range"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Device List */
|
||||
.device-list {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.device-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.device-address {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.device-actions .btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.no-devices {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Scan Results */
|
||||
.scan-results {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.scan-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.scan-item:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.scan-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.scan-rssi {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Call Info */
|
||||
#call-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#call-state {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#call-remote {
|
||||
font-size: 1.25rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
#call-duration {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Status Colors */
|
||||
.status-connected {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
header h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
/**
|
||||
* Web API - REST-Endpoints für Konfiguration
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "web_api.h"
|
||||
#include "config/config_manager.h"
|
||||
#include "wifi/wifi_manager.h"
|
||||
#include "bluetooth/bt_manager.h"
|
||||
#include "sip/sip_client.h"
|
||||
#include "audio/audio_router.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char* TAG = "WEB_API";
|
||||
|
||||
// Hilfsfunktion: JSON-Body aus Request lesen
|
||||
static cJSON* read_json_body(httpd_req_t* req)
|
||||
{
|
||||
int content_len = req->content_len;
|
||||
if (content_len <= 0 || content_len > 4096) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* buf = malloc(content_len + 1);
|
||||
if (!buf) return NULL;
|
||||
|
||||
int received = httpd_req_recv(req, buf, content_len);
|
||||
if (received <= 0) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
|
||||
cJSON* json = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
return json;
|
||||
}
|
||||
|
||||
// Hilfsfunktion: JSON-Antwort senden
|
||||
static esp_err_t send_json_response(httpd_req_t* req, cJSON* json)
|
||||
{
|
||||
char* str = cJSON_PrintUnformatted(json);
|
||||
if (!str) {
|
||||
httpd_resp_send_500(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, str, strlen(str));
|
||||
free(str);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t send_json_error(httpd_req_t* req, int status, const char* message)
|
||||
{
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(json, "success", false);
|
||||
cJSON_AddStringToObject(json, "error", message);
|
||||
|
||||
httpd_resp_set_status(req, status == 400 ? "400 Bad Request" :
|
||||
status == 404 ? "404 Not Found" : "500 Internal Server Error");
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t send_json_success(httpd_req_t* req, const char* message)
|
||||
{
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(json, "success", true);
|
||||
if (message) cJSON_AddStringToObject(json, "message", message);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============ Status API ============
|
||||
|
||||
static esp_err_t api_status_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
|
||||
// WiFi Status
|
||||
cJSON* wifi = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(wifi, "state",
|
||||
wifi_manager_get_state() == WIFI_STATE_STA_CONNECTED ? "connected" :
|
||||
wifi_manager_get_state() == WIFI_STATE_AP_STARTED ? "hotspot" : "disconnected");
|
||||
|
||||
char ip[16] = {0};
|
||||
wifi_manager_get_ip(ip, sizeof(ip));
|
||||
cJSON_AddStringToObject(wifi, "ip", ip);
|
||||
cJSON_AddStringToObject(wifi, "ssid", config->wifi.ssid);
|
||||
cJSON_AddItemToObject(json, "wifi", wifi);
|
||||
|
||||
// SIP Status
|
||||
cJSON* sip = cJSON_CreateObject();
|
||||
sip_reg_state_t reg_state = sip_client_get_reg_state();
|
||||
cJSON_AddStringToObject(sip, "state",
|
||||
reg_state == SIP_REG_STATE_REGISTERED ? "registered" :
|
||||
reg_state == SIP_REG_STATE_REGISTERING ? "registering" : "unregistered");
|
||||
cJSON_AddStringToObject(sip, "server", config->sip.server);
|
||||
cJSON_AddStringToObject(sip, "user", config->sip.username);
|
||||
cJSON_AddItemToObject(json, "sip", sip);
|
||||
|
||||
// Audio Status
|
||||
cJSON* audio = cJSON_CreateObject();
|
||||
audio_source_t source = audio_router_get_active_source();
|
||||
cJSON_AddStringToObject(audio, "source",
|
||||
source == AUDIO_SOURCE_USB ? "usb" :
|
||||
source == AUDIO_SOURCE_BLUETOOTH ? "bluetooth" : "none");
|
||||
cJSON_AddBoolToObject(audio, "usb_connected", audio_router_is_source_available(AUDIO_SOURCE_USB));
|
||||
cJSON_AddBoolToObject(audio, "bt_connected", audio_router_is_source_available(AUDIO_SOURCE_BLUETOOTH));
|
||||
cJSON_AddNumberToObject(audio, "volume", audio_router_get_volume());
|
||||
cJSON_AddBoolToObject(audio, "muted", audio_router_is_muted());
|
||||
cJSON_AddItemToObject(json, "audio", audio);
|
||||
|
||||
// Call Status
|
||||
cJSON* call = cJSON_CreateObject();
|
||||
sip_call_state_t call_state = sip_client_get_call_state();
|
||||
cJSON_AddStringToObject(call, "state",
|
||||
call_state == SIP_CALL_STATE_IDLE ? "idle" :
|
||||
call_state == SIP_CALL_STATE_INCOMING ? "incoming" :
|
||||
call_state == SIP_CALL_STATE_OUTGOING ? "outgoing" :
|
||||
call_state == SIP_CALL_STATE_RINGING ? "ringing" :
|
||||
call_state == SIP_CALL_STATE_CONNECTED ? "connected" : "unknown");
|
||||
|
||||
sip_call_info_t call_info;
|
||||
if (sip_client_get_call_info(&call_info) == ESP_OK && call_state != SIP_CALL_STATE_IDLE) {
|
||||
cJSON_AddStringToObject(call, "remote", call_info.remote_number);
|
||||
cJSON_AddStringToObject(call, "name", call_info.remote_name);
|
||||
cJSON_AddNumberToObject(call, "duration", call_info.duration_sec);
|
||||
}
|
||||
cJSON_AddItemToObject(json, "call", call);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============ WiFi API ============
|
||||
|
||||
static esp_err_t api_wifi_config_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "ssid", config->wifi.ssid);
|
||||
cJSON_AddStringToObject(json, "ip_mode", config->wifi.ip_mode == IP_MODE_DHCP ? "dhcp" : "static");
|
||||
cJSON_AddStringToObject(json, "static_ip", config->wifi.static_ip);
|
||||
cJSON_AddStringToObject(json, "gateway", config->wifi.gateway);
|
||||
cJSON_AddStringToObject(json, "netmask", config->wifi.netmask);
|
||||
cJSON_AddStringToObject(json, "dns", config->wifi.dns);
|
||||
cJSON_AddBoolToObject(json, "configured", config->wifi.configured);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t api_wifi_config_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
wifi_config_data_t wifi_cfg = {0};
|
||||
|
||||
cJSON* ssid = cJSON_GetObjectItem(json, "ssid");
|
||||
cJSON* password = cJSON_GetObjectItem(json, "password");
|
||||
cJSON* ip_mode = cJSON_GetObjectItem(json, "ip_mode");
|
||||
|
||||
if (!ssid || !cJSON_IsString(ssid) || strlen(ssid->valuestring) == 0) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "SSID required");
|
||||
}
|
||||
|
||||
strncpy(wifi_cfg.ssid, ssid->valuestring, CONFIG_MAX_SSID_LEN);
|
||||
if (password && cJSON_IsString(password)) {
|
||||
strncpy(wifi_cfg.password, password->valuestring, CONFIG_MAX_PASSWORD_LEN);
|
||||
}
|
||||
|
||||
wifi_cfg.ip_mode = IP_MODE_DHCP;
|
||||
if (ip_mode && cJSON_IsString(ip_mode) && strcmp(ip_mode->valuestring, "static") == 0) {
|
||||
wifi_cfg.ip_mode = IP_MODE_STATIC;
|
||||
|
||||
cJSON* static_ip = cJSON_GetObjectItem(json, "static_ip");
|
||||
cJSON* gateway = cJSON_GetObjectItem(json, "gateway");
|
||||
cJSON* netmask = cJSON_GetObjectItem(json, "netmask");
|
||||
cJSON* dns = cJSON_GetObjectItem(json, "dns");
|
||||
|
||||
if (static_ip && cJSON_IsString(static_ip))
|
||||
strncpy(wifi_cfg.static_ip, static_ip->valuestring, CONFIG_MAX_IP_LEN);
|
||||
if (gateway && cJSON_IsString(gateway))
|
||||
strncpy(wifi_cfg.gateway, gateway->valuestring, CONFIG_MAX_IP_LEN);
|
||||
if (netmask && cJSON_IsString(netmask))
|
||||
strncpy(wifi_cfg.netmask, netmask->valuestring, CONFIG_MAX_IP_LEN);
|
||||
if (dns && cJSON_IsString(dns))
|
||||
strncpy(wifi_cfg.dns, dns->valuestring, CONFIG_MAX_IP_LEN);
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
esp_err_t err = config_save_wifi(&wifi_cfg);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Save failed");
|
||||
}
|
||||
|
||||
// Neu verbinden
|
||||
wifi_manager_connect(&wifi_cfg);
|
||||
|
||||
return send_json_success(req, "WiFi configuration saved. Connecting...");
|
||||
}
|
||||
|
||||
static esp_err_t api_wifi_scan_get(httpd_req_t* req)
|
||||
{
|
||||
wifi_manager_scan();
|
||||
|
||||
// Warten auf Scan-Ergebnis
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
|
||||
uint16_t num_networks = 0;
|
||||
esp_wifi_scan_get_ap_num(&num_networks);
|
||||
|
||||
if (num_networks > 20) num_networks = 20;
|
||||
|
||||
wifi_ap_record_t* records = malloc(sizeof(wifi_ap_record_t) * num_networks);
|
||||
if (!records) {
|
||||
return send_json_error(req, 500, "Memory error");
|
||||
}
|
||||
|
||||
esp_wifi_scan_get_ap_records(&num_networks, records);
|
||||
|
||||
cJSON* json = cJSON_CreateArray();
|
||||
for (int i = 0; i < num_networks; i++) {
|
||||
cJSON* ap = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(ap, "ssid", (char*)records[i].ssid);
|
||||
cJSON_AddNumberToObject(ap, "rssi", records[i].rssi);
|
||||
cJSON_AddNumberToObject(ap, "channel", records[i].primary);
|
||||
cJSON_AddBoolToObject(ap, "secure", records[i].authmode != WIFI_AUTH_OPEN);
|
||||
cJSON_AddItemToArray(json, ap);
|
||||
}
|
||||
|
||||
free(records);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============ SIP API ============
|
||||
|
||||
static esp_err_t api_sip_config_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "server", config->sip.server);
|
||||
cJSON_AddNumberToObject(json, "port", config->sip.port);
|
||||
cJSON_AddStringToObject(json, "username", config->sip.username);
|
||||
cJSON_AddStringToObject(json, "display_name", config->sip.display_name);
|
||||
cJSON_AddBoolToObject(json, "configured", config->sip.configured);
|
||||
// Passwort wird nicht zurückgegeben
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t api_sip_config_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
sip_config_data_t sip_cfg = {0};
|
||||
sip_cfg.port = CONFIG_BSC_SIP_DEFAULT_PORT;
|
||||
|
||||
cJSON* server = cJSON_GetObjectItem(json, "server");
|
||||
cJSON* port = cJSON_GetObjectItem(json, "port");
|
||||
cJSON* username = cJSON_GetObjectItem(json, "username");
|
||||
cJSON* password = cJSON_GetObjectItem(json, "password");
|
||||
cJSON* display_name = cJSON_GetObjectItem(json, "display_name");
|
||||
|
||||
if (!server || !cJSON_IsString(server) || strlen(server->valuestring) == 0) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Server required");
|
||||
}
|
||||
if (!username || !cJSON_IsString(username) || strlen(username->valuestring) == 0) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Username required");
|
||||
}
|
||||
|
||||
strncpy(sip_cfg.server, server->valuestring, CONFIG_MAX_SIP_SERVER_LEN);
|
||||
strncpy(sip_cfg.username, username->valuestring, CONFIG_MAX_SIP_USER_LEN);
|
||||
|
||||
if (port && cJSON_IsNumber(port)) {
|
||||
sip_cfg.port = (uint16_t)port->valueint;
|
||||
}
|
||||
if (password && cJSON_IsString(password)) {
|
||||
strncpy(sip_cfg.password, password->valuestring, CONFIG_MAX_PASSWORD_LEN);
|
||||
}
|
||||
if (display_name && cJSON_IsString(display_name)) {
|
||||
strncpy(sip_cfg.display_name, display_name->valuestring, CONFIG_MAX_SIP_USER_LEN);
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
esp_err_t err = config_save_sip(&sip_cfg);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Save failed");
|
||||
}
|
||||
|
||||
// Neu registrieren
|
||||
sip_client_unregister();
|
||||
sip_client_register();
|
||||
|
||||
return send_json_success(req, "SIP configuration saved. Registering...");
|
||||
}
|
||||
|
||||
// ============ Bluetooth API ============
|
||||
|
||||
static esp_err_t api_bluetooth_devices_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateArray();
|
||||
|
||||
for (int i = 0; i < config->bluetooth.device_count; i++) {
|
||||
const bt_device_config_t* dev = &config->bluetooth.devices[i];
|
||||
cJSON* device = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(device, "address", dev->address);
|
||||
cJSON_AddStringToObject(device, "name", dev->name);
|
||||
cJSON_AddBoolToObject(device, "paired", dev->paired);
|
||||
cJSON_AddBoolToObject(device, "auto_connect", dev->auto_connect);
|
||||
cJSON_AddNumberToObject(device, "priority", dev->priority);
|
||||
// TODO: Check if currently connected
|
||||
cJSON_AddItemToArray(json, device);
|
||||
}
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_scan_post(httpd_req_t* req)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starte Bluetooth-Scan");
|
||||
bt_manager_start_discovery();
|
||||
return send_json_success(req, "Scan started");
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_pair_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* address = cJSON_GetObjectItem(json, "address");
|
||||
if (!address || !cJSON_IsString(address)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Address required");
|
||||
}
|
||||
|
||||
esp_bd_addr_t addr;
|
||||
esp_err_t err = bt_str_to_addr(address->valuestring, addr);
|
||||
if (err != ESP_OK) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Invalid address format");
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
err = bt_manager_pair(addr);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Pairing failed");
|
||||
}
|
||||
|
||||
return send_json_success(req, "Pairing initiated");
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_unpair_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* address = cJSON_GetObjectItem(json, "address");
|
||||
if (!address || !cJSON_IsString(address)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Address required");
|
||||
}
|
||||
|
||||
esp_bd_addr_t addr;
|
||||
esp_err_t err = bt_str_to_addr(address->valuestring, addr);
|
||||
if (err != ESP_OK) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Invalid address format");
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
bt_manager_unpair(addr);
|
||||
config_remove_bt_device(address->valuestring);
|
||||
|
||||
return send_json_success(req, "Device removed");
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_connect_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* address = cJSON_GetObjectItem(json, "address");
|
||||
if (!address || !cJSON_IsString(address)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Address required");
|
||||
}
|
||||
|
||||
esp_bd_addr_t addr;
|
||||
esp_err_t err = bt_str_to_addr(address->valuestring, addr);
|
||||
cJSON_Delete(json);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 400, "Invalid address format");
|
||||
}
|
||||
|
||||
err = bt_manager_connect(addr);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Connection failed");
|
||||
}
|
||||
|
||||
return send_json_success(req, "Connecting...");
|
||||
}
|
||||
|
||||
// ============ Call API ============
|
||||
|
||||
static esp_err_t api_call_answer_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = sip_client_answer();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Answer failed");
|
||||
}
|
||||
return send_json_success(req, "Call answered");
|
||||
}
|
||||
|
||||
static esp_err_t api_call_hangup_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = sip_client_hangup();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Hangup failed");
|
||||
}
|
||||
return send_json_success(req, "Call ended");
|
||||
}
|
||||
|
||||
static esp_err_t api_call_reject_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = sip_client_reject();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Reject failed");
|
||||
}
|
||||
return send_json_success(req, "Call rejected");
|
||||
}
|
||||
|
||||
// ============ System API ============
|
||||
|
||||
static esp_err_t api_system_reboot_post(httpd_req_t* req)
|
||||
{
|
||||
send_json_success(req, "Rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_restart();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t api_system_factory_reset_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = config_factory_reset();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Reset failed");
|
||||
}
|
||||
send_json_success(req, "Factory reset complete. Rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_restart();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============ Route Registration ============
|
||||
|
||||
void web_api_register_handlers(httpd_handle_t server)
|
||||
{
|
||||
ESP_LOGI(TAG, "Registriere API-Handler");
|
||||
|
||||
// Status
|
||||
httpd_uri_t uri;
|
||||
|
||||
uri = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// WiFi
|
||||
uri = (httpd_uri_t){.uri = "/api/wifi/config", .method = HTTP_GET, .handler = api_wifi_config_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/wifi/config", .method = HTTP_POST, .handler = api_wifi_config_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = api_wifi_scan_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// SIP
|
||||
uri = (httpd_uri_t){.uri = "/api/sip/config", .method = HTTP_GET, .handler = api_sip_config_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/sip/config", .method = HTTP_POST, .handler = api_sip_config_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// Bluetooth
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/devices", .method = HTTP_GET, .handler = api_bluetooth_devices_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/scan", .method = HTTP_POST, .handler = api_bluetooth_scan_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/pair", .method = HTTP_POST, .handler = api_bluetooth_pair_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/unpair", .method = HTTP_POST, .handler = api_bluetooth_unpair_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/connect", .method = HTTP_POST, .handler = api_bluetooth_connect_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// Call
|
||||
uri = (httpd_uri_t){.uri = "/api/call/answer", .method = HTTP_POST, .handler = api_call_answer_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/call/hangup", .method = HTTP_POST, .handler = api_call_hangup_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/call/reject", .method = HTTP_POST, .handler = api_call_reject_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// System
|
||||
uri = (httpd_uri_t){.uri = "/api/system/reboot", .method = HTTP_POST, .handler = api_system_reboot_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/system/factory-reset", .method = HTTP_POST, .handler = api_system_factory_reset_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Registriert alle API-Handler beim Webserver
|
||||
*/
|
||||
void web_api_register_handlers(httpd_handle_t server);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Web Server - Konfigurations-Weboberfläche
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "web_server.h"
|
||||
#include "web_api.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
static const char* TAG = "WEB_SRV";
|
||||
|
||||
// Embedded Files (aus CMakeLists.txt EMBED_FILES)
|
||||
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
|
||||
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
|
||||
extern const uint8_t style_css_start[] asm("_binary_style_css_start");
|
||||
extern const uint8_t style_css_end[] asm("_binary_style_css_end");
|
||||
extern const uint8_t app_js_start[] asm("_binary_app_js_start");
|
||||
extern const uint8_t app_js_end[] asm("_binary_app_js_end");
|
||||
|
||||
static httpd_handle_t s_server = NULL;
|
||||
|
||||
// Handler für statische Dateien
|
||||
static esp_err_t index_handler(httpd_req_t* req)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, (const char*)index_html_start, index_html_end - index_html_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t style_handler(httpd_req_t* req)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/css");
|
||||
httpd_resp_send(req, (const char*)style_css_start, style_css_end - style_css_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t js_handler(httpd_req_t* req)
|
||||
{
|
||||
httpd_resp_set_type(req, "application/javascript");
|
||||
httpd_resp_send(req, (const char*)app_js_start, app_js_end - app_js_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_init(void)
|
||||
{
|
||||
if (s_server != NULL) {
|
||||
ESP_LOGW(TAG, "Server bereits gestartet");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starte Webserver auf Port %d", CONFIG_BSC_WEB_PORT);
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = CONFIG_BSC_WEB_PORT;
|
||||
config.lru_purge_enable = true;
|
||||
config.max_uri_handlers = 20;
|
||||
config.stack_size = 8192;
|
||||
|
||||
esp_err_t err = httpd_start(&s_server, &config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Server starten fehlgeschlagen: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Statische Routen registrieren
|
||||
httpd_uri_t index_uri = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = index_handler,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &index_uri);
|
||||
|
||||
httpd_uri_t style_uri = {
|
||||
.uri = "/style.css",
|
||||
.method = HTTP_GET,
|
||||
.handler = style_handler,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &style_uri);
|
||||
|
||||
httpd_uri_t js_uri = {
|
||||
.uri = "/app.js",
|
||||
.method = HTTP_GET,
|
||||
.handler = js_handler,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &js_uri);
|
||||
|
||||
// API-Routen registrieren
|
||||
web_api_register_handlers(s_server);
|
||||
|
||||
ESP_LOGI(TAG, "Webserver gestartet");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_stop(void)
|
||||
{
|
||||
if (s_server == NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stoppe Webserver");
|
||||
esp_err_t err = httpd_stop(s_server);
|
||||
s_server = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_handle_t web_server_get_handle(void)
|
||||
{
|
||||
return s_server;
|
||||
}
|
||||
|
||||
esp_err_t web_server_send_ws_event(const char* event_type, const char* json_data)
|
||||
{
|
||||
// WebSocket-Broadcast - wird in einer erweiterten Version implementiert
|
||||
ESP_LOGD(TAG, "WS Event: %s", event_type);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Initialisiert und startet den Webserver
|
||||
*/
|
||||
esp_err_t web_server_init(void);
|
||||
|
||||
/**
|
||||
* Stoppt den Webserver
|
||||
*/
|
||||
esp_err_t web_server_stop(void);
|
||||
|
||||
/**
|
||||
* Gibt das Webserver-Handle zurück
|
||||
*/
|
||||
httpd_handle_t web_server_get_handle(void);
|
||||
|
||||
/**
|
||||
* Sendet ein WebSocket Event an alle verbundenen Clients
|
||||
*/
|
||||
esp_err_t web_server_send_ws_event(const char* event_type, const char* json_data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
/**
|
||||
* WiFi Manager - Verwaltet WiFi AP und Station Mode
|
||||
*
|
||||
* - Hotspot-Modus wenn keine WLAN-Daten konfiguriert
|
||||
* - Station-Modus wenn WLAN konfiguriert
|
||||
* - Automatischer Fallback zu Hotspot bei Verbindungsproblemen
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "wifi_manager.h"
|
||||
#include "config/config_manager.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sys.h"
|
||||
|
||||
static const char* TAG = "WIFI_MGR";
|
||||
|
||||
// Event Bits
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
// Maximale Wiederholungsversuche
|
||||
#define MAX_RETRY_COUNT 5
|
||||
|
||||
// State
|
||||
static wifi_state_t s_state = WIFI_STATE_IDLE;
|
||||
static EventGroupHandle_t s_wifi_event_group;
|
||||
static esp_netif_t* s_netif_ap = NULL;
|
||||
static esp_netif_t* s_netif_sta = NULL;
|
||||
static int s_retry_count = 0;
|
||||
static wifi_event_callback_t s_callback = NULL;
|
||||
static bool s_initialized = false;
|
||||
|
||||
// Forward Declarations
|
||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data);
|
||||
static void ip_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data);
|
||||
|
||||
static void notify_callback(wifi_state_t state, void* data)
|
||||
{
|
||||
s_state = state;
|
||||
if (s_callback) {
|
||||
s_callback(state, data);
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_base == WIFI_EVENT) {
|
||||
switch (event_id) {
|
||||
case WIFI_EVENT_AP_START:
|
||||
ESP_LOGI(TAG, "AP gestartet");
|
||||
notify_callback(WIFI_STATE_AP_STARTED, NULL);
|
||||
break;
|
||||
|
||||
case WIFI_EVENT_AP_STOP:
|
||||
ESP_LOGI(TAG, "AP gestoppt");
|
||||
break;
|
||||
|
||||
case WIFI_EVENT_AP_STACONNECTED: {
|
||||
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
|
||||
ESP_LOGI(TAG, "Client verbunden: " MACSTR, MAC2STR(event->mac));
|
||||
break;
|
||||
}
|
||||
|
||||
case WIFI_EVENT_AP_STADISCONNECTED: {
|
||||
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
|
||||
ESP_LOGI(TAG, "Client getrennt: " MACSTR, MAC2STR(event->mac));
|
||||
break;
|
||||
}
|
||||
|
||||
case WIFI_EVENT_STA_START:
|
||||
ESP_LOGI(TAG, "STA gestartet, verbinde...");
|
||||
esp_wifi_connect();
|
||||
notify_callback(WIFI_STATE_STA_CONNECTING, NULL);
|
||||
break;
|
||||
|
||||
case WIFI_EVENT_STA_DISCONNECTED: {
|
||||
wifi_event_sta_disconnected_t* event = (wifi_event_sta_disconnected_t*)event_data;
|
||||
ESP_LOGW(TAG, "Verbindung getrennt (Grund: %d)", event->reason);
|
||||
|
||||
if (s_retry_count < MAX_RETRY_COUNT) {
|
||||
s_retry_count++;
|
||||
ESP_LOGI(TAG, "Wiederverbinden... (Versuch %d/%d)", s_retry_count, MAX_RETRY_COUNT);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
esp_wifi_connect();
|
||||
notify_callback(WIFI_STATE_STA_DISCONNECTED, NULL);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Max. Versuche erreicht - Fallback zu AP");
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
notify_callback(WIFI_STATE_STA_FAILED, NULL);
|
||||
// Starte AP
|
||||
wifi_manager_start_ap();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case WIFI_EVENT_STA_CONNECTED:
|
||||
ESP_LOGI(TAG, "Mit AP verbunden");
|
||||
s_retry_count = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ip_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
|
||||
ESP_LOGI(TAG, "IP erhalten: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
s_retry_count = 0;
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
notify_callback(WIFI_STATE_STA_CONNECTED, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Initialisiere WiFi Manager");
|
||||
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
|
||||
// TCP/IP Stack initialisieren
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
// Netif für AP und STA erstellen
|
||||
s_netif_ap = esp_netif_create_default_wifi_ap();
|
||||
s_netif_sta = esp_netif_create_default_wifi_sta();
|
||||
|
||||
// WiFi mit Standardkonfiguration initialisieren
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
// Event Handler registrieren
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler, NULL, NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||
&ip_event_handler, NULL, NULL));
|
||||
|
||||
// WiFi Storage
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||
|
||||
s_initialized = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_start(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starte WiFi...");
|
||||
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
if (config->wifi.configured) {
|
||||
// Station Mode - mit konfiguriertem WLAN verbinden
|
||||
return wifi_manager_connect(&config->wifi);
|
||||
} else {
|
||||
// AP Mode - Hotspot starten
|
||||
return wifi_manager_start_ap();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_stop(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Stoppe WiFi");
|
||||
return esp_wifi_stop();
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_start_ap(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starte Hotspot: %s", CONFIG_BSC_DEFAULT_AP_SSID);
|
||||
|
||||
// Stoppen falls bereits aktiv
|
||||
esp_wifi_stop();
|
||||
|
||||
// AP-IP konfigurieren
|
||||
esp_netif_ip_info_t ip_info;
|
||||
IP4_ADDR(&ip_info.ip, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.gw, 192, 168, 4, 1);
|
||||
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
|
||||
|
||||
esp_netif_dhcps_stop(s_netif_ap);
|
||||
esp_netif_set_ip_info(s_netif_ap, &ip_info);
|
||||
esp_netif_dhcps_start(s_netif_ap);
|
||||
|
||||
// WiFi AP Konfiguration
|
||||
wifi_config_t wifi_config = {
|
||||
.ap = {
|
||||
.ssid_len = strlen(CONFIG_BSC_DEFAULT_AP_SSID),
|
||||
.channel = 1,
|
||||
.max_connection = 4,
|
||||
.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.pmf_cfg = {
|
||||
.required = false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
strncpy((char*)wifi_config.ap.ssid, CONFIG_BSC_DEFAULT_AP_SSID, sizeof(wifi_config.ap.ssid));
|
||||
strncpy((char*)wifi_config.ap.password, CONFIG_BSC_DEFAULT_AP_PASSWORD, sizeof(wifi_config.ap.password));
|
||||
|
||||
if (strlen(CONFIG_BSC_DEFAULT_AP_PASSWORD) < 8) {
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "Hotspot gestartet. SSID: %s, Passwort: %s",
|
||||
CONFIG_BSC_DEFAULT_AP_SSID, CONFIG_BSC_DEFAULT_AP_PASSWORD);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_connect(const wifi_config_data_t* config)
|
||||
{
|
||||
if (!config || strlen(config->ssid) == 0) {
|
||||
ESP_LOGE(TAG, "Ungültige WiFi-Konfiguration");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Verbinde mit WLAN: %s", config->ssid);
|
||||
|
||||
// Stoppen falls bereits aktiv
|
||||
esp_wifi_stop();
|
||||
|
||||
// Static IP konfigurieren wenn gewünscht
|
||||
if (config->ip_mode == IP_MODE_STATIC && strlen(config->static_ip) > 0) {
|
||||
ESP_LOGI(TAG, "Verwende statische IP: %s", config->static_ip);
|
||||
|
||||
esp_netif_dhcpc_stop(s_netif_sta);
|
||||
|
||||
esp_netif_ip_info_t ip_info;
|
||||
memset(&ip_info, 0, sizeof(ip_info));
|
||||
|
||||
ip4addr_aton(config->static_ip, (ip4_addr_t*)&ip_info.ip);
|
||||
ip4addr_aton(config->gateway, (ip4_addr_t*)&ip_info.gw);
|
||||
ip4addr_aton(config->netmask, (ip4_addr_t*)&ip_info.netmask);
|
||||
|
||||
esp_netif_set_ip_info(s_netif_sta, &ip_info);
|
||||
|
||||
// DNS konfigurieren wenn vorhanden
|
||||
if (strlen(config->dns) > 0) {
|
||||
esp_netif_dns_info_t dns;
|
||||
ip4addr_aton(config->dns, (ip4_addr_t*)&dns.ip.u_addr.ip4);
|
||||
dns.ip.type = ESP_IPADDR_TYPE_V4;
|
||||
esp_netif_set_dns_info(s_netif_sta, ESP_NETIF_DNS_MAIN, &dns);
|
||||
}
|
||||
} else {
|
||||
esp_netif_dhcpc_start(s_netif_sta);
|
||||
}
|
||||
|
||||
// WiFi STA Konfiguration
|
||||
wifi_config_t wifi_config = {
|
||||
.sta = {
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
.sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
|
||||
},
|
||||
};
|
||||
|
||||
strncpy((char*)wifi_config.sta.ssid, config->ssid, sizeof(wifi_config.sta.ssid));
|
||||
strncpy((char*)wifi_config.sta.password, config->password, sizeof(wifi_config.sta.password));
|
||||
|
||||
s_retry_count = 0;
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_disconnect(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Trenne WiFi-Verbindung");
|
||||
esp_wifi_disconnect();
|
||||
return wifi_manager_start_ap();
|
||||
}
|
||||
|
||||
wifi_state_t wifi_manager_get_state(void)
|
||||
{
|
||||
return s_state;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_get_ip(char* ip_str, size_t len)
|
||||
{
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_t* netif = (s_state == WIFI_STATE_AP_STARTED) ? s_netif_ap : s_netif_sta;
|
||||
|
||||
esp_err_t err = esp_netif_get_ip_info(netif, &ip_info);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_scan(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starte WLAN-Scan...");
|
||||
|
||||
// Scan-Konfiguration
|
||||
wifi_scan_config_t scan_config = {
|
||||
.ssid = NULL,
|
||||
.bssid = NULL,
|
||||
.channel = 0,
|
||||
.show_hidden = false,
|
||||
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
|
||||
.scan_time.active.min = 100,
|
||||
.scan_time.active.max = 300,
|
||||
};
|
||||
|
||||
return esp_wifi_scan_start(&scan_config, false);
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_register_callback(wifi_event_callback_t callback)
|
||||
{
|
||||
s_callback = callback;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_netif_t* wifi_manager_get_netif(void)
|
||||
{
|
||||
return (s_state == WIFI_STATE_AP_STARTED) ? s_netif_ap : s_netif_sta;
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_netif.h"
|
||||
#include "config/config_manager.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// WiFi-Status
|
||||
typedef enum {
|
||||
WIFI_STATE_IDLE = 0,
|
||||
WIFI_STATE_AP_STARTED, // Hotspot läuft
|
||||
WIFI_STATE_STA_CONNECTING, // Verbinde mit WLAN
|
||||
WIFI_STATE_STA_CONNECTED, // Mit WLAN verbunden
|
||||
WIFI_STATE_STA_DISCONNECTED,// Verbindung verloren
|
||||
WIFI_STATE_STA_FAILED // Verbindung fehlgeschlagen -> Fallback zu AP
|
||||
} wifi_state_t;
|
||||
|
||||
// WiFi-Event Callback
|
||||
typedef void (*wifi_event_callback_t)(wifi_state_t state, void* data);
|
||||
|
||||
/**
|
||||
* Initialisiert den WiFi-Manager
|
||||
* Startet automatisch im AP-Modus wenn keine WLAN-Daten konfiguriert sind
|
||||
*/
|
||||
esp_err_t wifi_manager_init(void);
|
||||
|
||||
/**
|
||||
* Startet den WiFi-Manager
|
||||
* - Wenn WLAN konfiguriert: Verbindet als Station
|
||||
* - Wenn nicht konfiguriert: Startet Hotspot
|
||||
*/
|
||||
esp_err_t wifi_manager_start(void);
|
||||
|
||||
/**
|
||||
* Stoppt den WiFi-Manager
|
||||
*/
|
||||
esp_err_t wifi_manager_stop(void);
|
||||
|
||||
/**
|
||||
* Verbindet mit einem WLAN-Netzwerk
|
||||
*/
|
||||
esp_err_t wifi_manager_connect(const wifi_config_data_t* config);
|
||||
|
||||
/**
|
||||
* Trennt die WLAN-Verbindung und startet Hotspot
|
||||
*/
|
||||
esp_err_t wifi_manager_disconnect(void);
|
||||
|
||||
/**
|
||||
* Startet explizit den Hotspot-Modus
|
||||
*/
|
||||
esp_err_t wifi_manager_start_ap(void);
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Status zurück
|
||||
*/
|
||||
wifi_state_t wifi_manager_get_state(void);
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle IP-Adresse zurück
|
||||
*/
|
||||
esp_err_t wifi_manager_get_ip(char* ip_str, size_t len);
|
||||
|
||||
/**
|
||||
* Scannt nach verfügbaren WLAN-Netzwerken
|
||||
*/
|
||||
esp_err_t wifi_manager_scan(void);
|
||||
|
||||
/**
|
||||
* Registriert einen Event-Callback
|
||||
*/
|
||||
esp_err_t wifi_manager_register_callback(wifi_event_callback_t callback);
|
||||
|
||||
/**
|
||||
* Gibt das netif Handle zurück
|
||||
*/
|
||||
esp_netif_t* wifi_manager_get_netif(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# ESP32-S3 Bluetooth SIP Client Partition Table
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
phy_init, data, phy, 0xf000, 0x1000,
|
||||
factory, app, factory, 0x10000, 0x300000,
|
||||
storage, data, spiffs, 0x310000, 0xF0000,
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
# ESP32-S3 Bluetooth SIP Client - Default Configuration
|
||||
|
||||
# Target: ESP32-S3
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
|
||||
# Flash Größe
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
|
||||
# PSRAM aktivieren (falls vorhanden auf DevKitC)
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
|
||||
# Bluetooth aktivieren
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=y
|
||||
CONFIG_BT_CLASSIC_ENABLED=y
|
||||
CONFIG_BT_A2DP_ENABLE=y
|
||||
CONFIG_BT_HFP_ENABLE=y
|
||||
CONFIG_BT_HFP_AG_ENABLE=y
|
||||
CONFIG_BT_HFP_CLIENT_ENABLE=y
|
||||
CONFIG_BT_SSP_ENABLED=y
|
||||
CONFIG_BT_BLE_ENABLED=y
|
||||
|
||||
# Bluetooth Controller
|
||||
CONFIG_BTDM_CTRL_MODE_BTDM=y
|
||||
CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y
|
||||
|
||||
# USB Host
|
||||
CONFIG_USB_OTG_SUPPORTED=y
|
||||
CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=1024
|
||||
|
||||
# WiFi
|
||||
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y
|
||||
CONFIG_ESP_WIFI_NVS_ENABLED=y
|
||||
|
||||
# LWIP
|
||||
CONFIG_LWIP_IPV4=y
|
||||
CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y
|
||||
|
||||
# HTTP Server
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
|
||||
CONFIG_HTTPD_MAX_URI_LEN=512
|
||||
|
||||
# NVS
|
||||
CONFIG_NVS_ENCRYPTION=n
|
||||
|
||||
# Logging
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
||||
|
||||
# FreeRTOS
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3072
|
||||
|
||||
# Compiler Optimierung
|
||||
CONFIG_COMPILER_OPTIMIZATION_PERF=y
|
||||
Loading…
Reference in New Issue