first commit

This commit is contained in:
Stefan Hacker 2026-01-29 20:31:37 +01:00
commit 0b09765013
30 changed files with 6865 additions and 0 deletions

11
CMakeLists.txt Normal file
View File

@ -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)

171
README.md Normal file
View File

@ -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

39
main/CMakeLists.txt Normal file
View File

@ -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
)

57
main/Kconfig.projbuild Normal file
View File

@ -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

401
main/audio/audio_router.c Normal file
View File

@ -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;
}

128
main/audio/audio_router.h Normal file
View File

@ -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

324
main/bluetooth/bt_hfp.c Normal file
View File

@ -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;
}

49
main/bluetooth/bt_hfp.h Normal file
View File

@ -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

455
main/bluetooth/bt_manager.c Normal file
View File

@ -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 = &param->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);
}
}

145
main/bluetooth/bt_manager.h Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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

295
main/main.c Normal file
View File

@ -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]);
}
}

1033
main/sip/sip_client.c Normal file

File diff suppressed because it is too large Load Diff

139
main/sip/sip_client.h Normal file
View File

@ -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

345
main/sip/sip_parser.c Normal file
View File

@ -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;
}

92
main/sip/sip_parser.h Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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

443
main/web/static/app.js Normal file
View File

@ -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();
}
})();

183
main/web/static/index.html Normal file
View File

@ -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>

374
main/web/static/style.css Normal file
View File

@ -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;
}
}

548
main/web/web_api.c Normal file
View File

@ -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);
}

16
main/web/web_api.h Normal file
View File

@ -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

117
main/web/web_server.c Normal file
View File

@ -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;
}

32
main/web/web_server.h Normal file
View File

@ -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

340
main/wifi/wifi_manager.c Normal file
View File

@ -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;
}

85
main/wifi/wifi_manager.h Normal file
View File

@ -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

6
partitions.csv Normal file
View File

@ -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,
1 # ESP32-S3 Bluetooth SIP Client Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 0x300000,
6 storage, data, spiffs, 0x310000, 0xF0000,

59
sdkconfig.defaults Normal file
View File

@ -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