first commit
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
// ESP32 SIP Phone - Web Interface
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// State
|
||||
let currentTab = 'status';
|
||||
let statusUpdateInterval = null;
|
||||
|
||||
// DOM Elements
|
||||
const elements = {
|
||||
// Status bar
|
||||
wifiStatus: document.getElementById('wifi-status'),
|
||||
sipStatus: document.getElementById('sip-status'),
|
||||
audioStatus: document.getElementById('audio-status'),
|
||||
|
||||
// Call info
|
||||
callState: document.getElementById('call-state'),
|
||||
callRemote: document.getElementById('call-remote'),
|
||||
callDuration: document.getElementById('call-duration'),
|
||||
callButtons: document.getElementById('call-buttons'),
|
||||
btnAnswer: document.getElementById('btn-answer'),
|
||||
btnReject: document.getElementById('btn-reject'),
|
||||
btnHangup: document.getElementById('btn-hangup'),
|
||||
|
||||
// Audio
|
||||
audioSource: document.getElementById('audio-source'),
|
||||
usbConnected: document.getElementById('usb-connected'),
|
||||
btConnected: document.getElementById('bt-connected'),
|
||||
volumeSlider: document.getElementById('volume-slider'),
|
||||
volumeValue: document.getElementById('volume-value'),
|
||||
muteCheckbox: document.getElementById('mute-checkbox'),
|
||||
|
||||
// WiFi
|
||||
wifiForm: document.getElementById('wifi-form'),
|
||||
wifiSsid: document.getElementById('wifi-ssid'),
|
||||
wifiPassword: document.getElementById('wifi-password'),
|
||||
btnScanWifi: document.getElementById('btn-scan-wifi'),
|
||||
wifiScanResults: document.getElementById('wifi-scan-results'),
|
||||
staticIpConfig: document.getElementById('static-ip-config'),
|
||||
|
||||
// SIP
|
||||
sipForm: document.getElementById('sip-form'),
|
||||
sipServer: document.getElementById('sip-server'),
|
||||
sipPort: document.getElementById('sip-port'),
|
||||
sipUsername: document.getElementById('sip-username'),
|
||||
sipPassword: document.getElementById('sip-password'),
|
||||
sipDisplayName: document.getElementById('sip-display-name'),
|
||||
|
||||
// Bluetooth
|
||||
btnScanBt: document.getElementById('btn-scan-bt'),
|
||||
btPairedDevices: document.getElementById('bt-paired-devices'),
|
||||
btFoundDevices: document.getElementById('bt-found-devices'),
|
||||
|
||||
// System
|
||||
btnReboot: document.getElementById('btn-reboot'),
|
||||
btnFactoryReset: document.getElementById('btn-factory-reset')
|
||||
};
|
||||
|
||||
// API Functions
|
||||
async function apiGet(endpoint) {
|
||||
try {
|
||||
const response = await fetch('/api/' + endpoint);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiPost(endpoint, data = {}) {
|
||||
try {
|
||||
const response = await fetch('/api/' + endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Status Updates
|
||||
async function updateStatus() {
|
||||
const status = await apiGet('status');
|
||||
if (!status) return;
|
||||
|
||||
// WiFi
|
||||
if (status.wifi) {
|
||||
const wifiState = status.wifi.state === 'connected' ? 'Verbunden' :
|
||||
status.wifi.state === 'hotspot' ? 'Hotspot' : 'Getrennt';
|
||||
elements.wifiStatus.textContent = 'WiFi: ' + wifiState;
|
||||
elements.wifiStatus.className = 'status-item ' +
|
||||
(status.wifi.state === 'connected' ? 'status-connected' : 'status-disconnected');
|
||||
}
|
||||
|
||||
// SIP
|
||||
if (status.sip) {
|
||||
const sipState = status.sip.state === 'registered' ? 'Registriert' :
|
||||
status.sip.state === 'registering' ? 'Verbinde...' : 'Nicht registriert';
|
||||
elements.sipStatus.textContent = 'SIP: ' + sipState;
|
||||
elements.sipStatus.className = 'status-item ' +
|
||||
(status.sip.state === 'registered' ? 'status-connected' :
|
||||
status.sip.state === 'registering' ? 'status-pending' : 'status-disconnected');
|
||||
}
|
||||
|
||||
// Audio
|
||||
if (status.audio) {
|
||||
const source = status.audio.source === 'usb' ? 'USB' :
|
||||
status.audio.source === 'bluetooth' ? 'Bluetooth' : 'Keine';
|
||||
elements.audioStatus.textContent = 'Audio: ' + source;
|
||||
elements.audioSource.textContent = source;
|
||||
elements.usbConnected.textContent = status.audio.usb_connected ? 'Verbunden' : 'Nicht verbunden';
|
||||
elements.btConnected.textContent = status.audio.bt_connected ? 'Verbunden' : 'Nicht verbunden';
|
||||
elements.volumeSlider.value = status.audio.volume;
|
||||
elements.volumeValue.textContent = status.audio.volume + '%';
|
||||
elements.muteCheckbox.checked = status.audio.muted;
|
||||
}
|
||||
|
||||
// Call
|
||||
if (status.call) {
|
||||
updateCallUI(status.call);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCallUI(call) {
|
||||
const states = {
|
||||
'idle': 'Kein aktiver Anruf',
|
||||
'incoming': 'Eingehender Anruf',
|
||||
'outgoing': 'Ausgehender Anruf',
|
||||
'ringing': 'Klingelt...',
|
||||
'connected': 'Verbunden'
|
||||
};
|
||||
|
||||
elements.callState.textContent = states[call.state] || call.state;
|
||||
|
||||
if (call.state !== 'idle') {
|
||||
elements.callRemote.textContent = (call.name || '') + ' ' + (call.remote || '');
|
||||
if (call.state === 'connected' && call.duration !== undefined) {
|
||||
const mins = Math.floor(call.duration / 60);
|
||||
const secs = call.duration % 60;
|
||||
elements.callDuration.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
|
||||
} else {
|
||||
elements.callDuration.textContent = '';
|
||||
}
|
||||
elements.callButtons.classList.remove('hidden');
|
||||
|
||||
// Show appropriate buttons
|
||||
elements.btnAnswer.classList.toggle('hidden', call.state !== 'incoming');
|
||||
elements.btnReject.classList.toggle('hidden', call.state !== 'incoming');
|
||||
elements.btnHangup.classList.toggle('hidden', call.state === 'idle' || call.state === 'incoming');
|
||||
} else {
|
||||
elements.callRemote.textContent = '';
|
||||
elements.callDuration.textContent = '';
|
||||
elements.callButtons.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Tab Navigation
|
||||
function setupTabs() {
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tab = btn.dataset.tab;
|
||||
if (tab === currentTab) return;
|
||||
|
||||
// Update nav
|
||||
document.querySelector('.nav-btn.active').classList.remove('active');
|
||||
btn.classList.add('active');
|
||||
|
||||
// Update content
|
||||
document.querySelector('.tab-content.active').classList.remove('active');
|
||||
document.getElementById('tab-' + tab).classList.add('active');
|
||||
|
||||
currentTab = tab;
|
||||
|
||||
// Load tab-specific data
|
||||
if (tab === 'wifi') loadWifiConfig();
|
||||
if (tab === 'sip') loadSipConfig();
|
||||
if (tab === 'bluetooth') loadBluetoothDevices();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// WiFi
|
||||
async function loadWifiConfig() {
|
||||
const config = await apiGet('wifi/config');
|
||||
if (!config) return;
|
||||
|
||||
elements.wifiSsid.value = config.ssid || '';
|
||||
elements.wifiPassword.value = '';
|
||||
|
||||
const ipMode = config.ip_mode || 'dhcp';
|
||||
document.querySelector('input[name="ip-mode"][value="' + ipMode + '"]').checked = true;
|
||||
elements.staticIpConfig.classList.toggle('hidden', ipMode !== 'static');
|
||||
|
||||
if (ipMode === 'static') {
|
||||
document.getElementById('static-ip').value = config.static_ip || '';
|
||||
document.getElementById('gateway').value = config.gateway || '';
|
||||
document.getElementById('netmask').value = config.netmask || '';
|
||||
document.getElementById('dns').value = config.dns || '';
|
||||
}
|
||||
}
|
||||
|
||||
async function scanWifi() {
|
||||
elements.btnScanWifi.disabled = true;
|
||||
elements.btnScanWifi.textContent = 'Scanne...';
|
||||
elements.wifiScanResults.classList.remove('hidden');
|
||||
elements.wifiScanResults.innerHTML = '<p class="no-devices">Scanne...</p>';
|
||||
|
||||
const networks = await apiGet('wifi/scan');
|
||||
|
||||
elements.btnScanWifi.disabled = false;
|
||||
elements.btnScanWifi.textContent = 'Scannen';
|
||||
|
||||
if (!networks || networks.length === 0) {
|
||||
elements.wifiScanResults.innerHTML = '<p class="no-devices">Keine Netzwerke gefunden</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
elements.wifiScanResults.innerHTML = networks.map(net =>
|
||||
'<div class="scan-item" data-ssid="' + escapeHtml(net.ssid) + '">' +
|
||||
'<span>' + escapeHtml(net.ssid) + (net.secure ? ' 🔒' : '') + '</span>' +
|
||||
'<span class="scan-rssi">' + net.rssi + ' dBm</span>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
|
||||
elements.wifiScanResults.querySelectorAll('.scan-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
elements.wifiSsid.value = item.dataset.ssid;
|
||||
elements.wifiScanResults.classList.add('hidden');
|
||||
elements.wifiPassword.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveWifiConfig(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const ipMode = document.querySelector('input[name="ip-mode"]:checked').value;
|
||||
const data = {
|
||||
ssid: elements.wifiSsid.value,
|
||||
password: elements.wifiPassword.value,
|
||||
ip_mode: ipMode
|
||||
};
|
||||
|
||||
if (ipMode === 'static') {
|
||||
data.static_ip = document.getElementById('static-ip').value;
|
||||
data.gateway = document.getElementById('gateway').value;
|
||||
data.netmask = document.getElementById('netmask').value;
|
||||
data.dns = document.getElementById('dns').value;
|
||||
}
|
||||
|
||||
const result = await apiPost('wifi/config', data);
|
||||
if (result && result.success) {
|
||||
alert('WiFi-Konfiguration gespeichert. Verbinde...');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
// SIP
|
||||
async function loadSipConfig() {
|
||||
const config = await apiGet('sip/config');
|
||||
if (!config) return;
|
||||
|
||||
elements.sipServer.value = config.server || '';
|
||||
elements.sipPort.value = config.port || 5060;
|
||||
elements.sipUsername.value = config.username || '';
|
||||
elements.sipPassword.value = '';
|
||||
elements.sipDisplayName.value = config.display_name || '';
|
||||
}
|
||||
|
||||
async function saveSipConfig(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
server: elements.sipServer.value,
|
||||
port: parseInt(elements.sipPort.value) || 5060,
|
||||
username: elements.sipUsername.value,
|
||||
password: elements.sipPassword.value,
|
||||
display_name: elements.sipDisplayName.value
|
||||
};
|
||||
|
||||
const result = await apiPost('sip/config', data);
|
||||
if (result && result.success) {
|
||||
alert('SIP-Konfiguration gespeichert. Registriere...');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth
|
||||
async function loadBluetoothDevices() {
|
||||
const devices = await apiGet('bluetooth/devices');
|
||||
|
||||
if (!devices || devices.length === 0) {
|
||||
elements.btPairedDevices.innerHTML = '<p class="no-devices">Keine Gerate gepaart</p>';
|
||||
} else {
|
||||
elements.btPairedDevices.innerHTML = devices.map(dev =>
|
||||
'<div class="device-item">' +
|
||||
'<div class="device-info">' +
|
||||
'<div class="device-name">' + escapeHtml(dev.name || 'Unbekannt') + '</div>' +
|
||||
'<div class="device-address">' + escapeHtml(dev.address) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="device-actions">' +
|
||||
'<button class="btn btn-secondary btn-connect" data-address="' + escapeHtml(dev.address) + '">Verbinden</button>' +
|
||||
'<button class="btn btn-danger btn-unpair" data-address="' + escapeHtml(dev.address) + '">Entfernen</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
|
||||
elements.btPairedDevices.querySelectorAll('.btn-connect').forEach(btn => {
|
||||
btn.addEventListener('click', () => connectBluetooth(btn.dataset.address));
|
||||
});
|
||||
|
||||
elements.btPairedDevices.querySelectorAll('.btn-unpair').forEach(btn => {
|
||||
btn.addEventListener('click', () => unpairBluetooth(btn.dataset.address));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function scanBluetooth() {
|
||||
elements.btnScanBt.disabled = true;
|
||||
elements.btnScanBt.textContent = 'Suche...';
|
||||
elements.btFoundDevices.innerHTML = '<p class="no-devices">Suche Gerate...</p>';
|
||||
|
||||
await apiPost('bluetooth/scan');
|
||||
|
||||
// Wait for scan to complete
|
||||
setTimeout(async () => {
|
||||
elements.btnScanBt.disabled = false;
|
||||
elements.btnScanBt.textContent = 'Gerate suchen';
|
||||
// In a full implementation, we'd poll for discovered devices
|
||||
elements.btFoundDevices.innerHTML = '<p class="no-devices">Suche abgeschlossen. Gerate werden automatisch gepaart wenn sie in den Pairing-Modus gehen.</p>';
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async function connectBluetooth(address) {
|
||||
const result = await apiPost('bluetooth/connect', { address });
|
||||
if (result && result.success) {
|
||||
alert('Verbinde...');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
async function unpairBluetooth(address) {
|
||||
if (!confirm('Gerat wirklich entfernen?')) return;
|
||||
|
||||
const result = await apiPost('bluetooth/unpair', { address });
|
||||
if (result && result.success) {
|
||||
loadBluetoothDevices();
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
}
|
||||
|
||||
// Call Actions
|
||||
async function answerCall() {
|
||||
await apiPost('call/answer');
|
||||
}
|
||||
|
||||
async function rejectCall() {
|
||||
await apiPost('call/reject');
|
||||
}
|
||||
|
||||
async function hangupCall() {
|
||||
await apiPost('call/hangup');
|
||||
}
|
||||
|
||||
// System
|
||||
async function reboot() {
|
||||
if (!confirm('System wirklich neustarten?')) return;
|
||||
await apiPost('system/reboot');
|
||||
alert('System startet neu...');
|
||||
}
|
||||
|
||||
async function factoryReset() {
|
||||
if (!confirm('ACHTUNG: Alle Einstellungen werden geloscht! Fortfahren?')) return;
|
||||
if (!confirm('Wirklich alle Einstellungen loschen?')) return;
|
||||
await apiPost('system/factory-reset');
|
||||
alert('Werksreset durchgefuhrt. System startet neu...');
|
||||
}
|
||||
|
||||
// Utilities
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
function setupEventListeners() {
|
||||
// Call buttons
|
||||
elements.btnAnswer.addEventListener('click', answerCall);
|
||||
elements.btnReject.addEventListener('click', rejectCall);
|
||||
elements.btnHangup.addEventListener('click', hangupCall);
|
||||
|
||||
// Volume
|
||||
elements.volumeSlider.addEventListener('input', () => {
|
||||
elements.volumeValue.textContent = elements.volumeSlider.value + '%';
|
||||
});
|
||||
|
||||
// WiFi
|
||||
elements.wifiForm.addEventListener('submit', saveWifiConfig);
|
||||
elements.btnScanWifi.addEventListener('click', scanWifi);
|
||||
|
||||
document.querySelectorAll('input[name="ip-mode"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
elements.staticIpConfig.classList.toggle('hidden', e.target.value !== 'static');
|
||||
});
|
||||
});
|
||||
|
||||
// SIP
|
||||
elements.sipForm.addEventListener('submit', saveSipConfig);
|
||||
|
||||
// Bluetooth
|
||||
elements.btnScanBt.addEventListener('click', scanBluetooth);
|
||||
|
||||
// System
|
||||
elements.btnReboot.addEventListener('click', reboot);
|
||||
elements.btnFactoryReset.addEventListener('click', factoryReset);
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
setupTabs();
|
||||
setupEventListeners();
|
||||
updateStatus();
|
||||
|
||||
// Update status every 2 seconds
|
||||
statusUpdateInterval = setInterval(updateStatus, 2000);
|
||||
}
|
||||
|
||||
// Start
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 SIP Phone</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>ESP32 SIP Phone</h1>
|
||||
<div id="status-bar">
|
||||
<span id="wifi-status" class="status-item">WiFi: --</span>
|
||||
<span id="sip-status" class="status-item">SIP: --</span>
|
||||
<span id="audio-status" class="status-item">Audio: --</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<button class="nav-btn active" data-tab="status">Status</button>
|
||||
<button class="nav-btn" data-tab="wifi">WLAN</button>
|
||||
<button class="nav-btn" data-tab="sip">SIP</button>
|
||||
<button class="nav-btn" data-tab="bluetooth">Bluetooth</button>
|
||||
<button class="nav-btn" data-tab="system">System</button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<!-- Status Tab -->
|
||||
<section id="tab-status" class="tab-content active">
|
||||
<div class="card">
|
||||
<h2>Anrufstatus</h2>
|
||||
<div id="call-info">
|
||||
<p id="call-state">Kein aktiver Anruf</p>
|
||||
<p id="call-remote"></p>
|
||||
<p id="call-duration"></p>
|
||||
</div>
|
||||
<div id="call-buttons" class="button-group hidden">
|
||||
<button id="btn-answer" class="btn btn-success">Annehmen</button>
|
||||
<button id="btn-reject" class="btn btn-danger">Ablehnen</button>
|
||||
<button id="btn-hangup" class="btn btn-danger">Auflegen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Audio</h2>
|
||||
<p>Aktive Quelle: <strong id="audio-source">--</strong></p>
|
||||
<p>USB Headset: <span id="usb-connected">--</span></p>
|
||||
<p>Bluetooth Headset: <span id="bt-connected">--</span></p>
|
||||
<div class="volume-control">
|
||||
<label>Lautstarke:</label>
|
||||
<input type="range" id="volume-slider" min="0" max="100" value="80">
|
||||
<span id="volume-value">80%</span>
|
||||
</div>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="mute-checkbox"> Stummschalten
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- WiFi Tab -->
|
||||
<section id="tab-wifi" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>WLAN-Konfiguration</h2>
|
||||
<form id="wifi-form">
|
||||
<div class="form-group">
|
||||
<label for="wifi-ssid">SSID:</label>
|
||||
<div class="input-with-button">
|
||||
<input type="text" id="wifi-ssid" required>
|
||||
<button type="button" id="btn-scan-wifi" class="btn btn-secondary">Scannen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wifi-scan-results" class="scan-results hidden"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wifi-password">Passwort:</label>
|
||||
<input type="password" id="wifi-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>IP-Konfiguration:</label>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="ip-mode" value="dhcp" checked> DHCP</label>
|
||||
<label><input type="radio" name="ip-mode" value="static"> Statisch</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="static-ip-config" class="hidden">
|
||||
<div class="form-group">
|
||||
<label for="static-ip">IP-Adresse:</label>
|
||||
<input type="text" id="static-ip" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gateway">Gateway:</label>
|
||||
<input type="text" id="gateway" placeholder="192.168.1.1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="netmask">Netzmaske:</label>
|
||||
<input type="text" id="netmask" placeholder="255.255.255.0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dns">DNS:</label>
|
||||
<input type="text" id="dns" placeholder="8.8.8.8">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SIP Tab -->
|
||||
<section id="tab-sip" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>SIP-Konfiguration</h2>
|
||||
<form id="sip-form">
|
||||
<div class="form-group">
|
||||
<label for="sip-server">Server:</label>
|
||||
<input type="text" id="sip-server" placeholder="pbx.example.com" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-port">Port:</label>
|
||||
<input type="number" id="sip-port" value="5060">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-username">Benutzername:</label>
|
||||
<input type="text" id="sip-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-password">Passwort:</label>
|
||||
<input type="password" id="sip-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sip-display-name">Anzeigename:</label>
|
||||
<input type="text" id="sip-display-name" placeholder="Max Mustermann">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bluetooth Tab -->
|
||||
<section id="tab-bluetooth" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>Bluetooth Headsets</h2>
|
||||
<p class="hint">USB-Headsets haben Vorrang vor Bluetooth.</p>
|
||||
|
||||
<button id="btn-scan-bt" class="btn btn-secondary">Geräte suchen</button>
|
||||
|
||||
<h3>Gepaarte Geräte</h3>
|
||||
<div id="bt-paired-devices" class="device-list">
|
||||
<p class="no-devices">Keine Geräte gepaart</p>
|
||||
</div>
|
||||
|
||||
<h3>Gefundene Geräte</h3>
|
||||
<div id="bt-found-devices" class="device-list">
|
||||
<p class="no-devices">Starte Suche...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- System Tab -->
|
||||
<section id="tab-system" class="tab-content">
|
||||
<div class="card">
|
||||
<h2>System</h2>
|
||||
<div class="button-group">
|
||||
<button id="btn-reboot" class="btn btn-secondary">Neustart</button>
|
||||
<button id="btn-factory-reset" class="btn btn-danger">Werksreset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Info</h2>
|
||||
<p>ESP32-S3 Bluetooth SIP Client</p>
|
||||
<p>Hotspot: ESP32-SIP-Phone</p>
|
||||
<p>Standard-IP: 192.168.4.1</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,374 @@
|
||||
/* ESP32 SIP Phone - Web Interface Styles */
|
||||
|
||||
:root {
|
||||
--primary: #2196F3;
|
||||
--primary-dark: #1976D2;
|
||||
--success: #4CAF50;
|
||||
--danger: #f44336;
|
||||
--warning: #FF9800;
|
||||
--bg: #f5f5f5;
|
||||
--card-bg: #ffffff;
|
||||
--text: #333333;
|
||||
--text-light: #666666;
|
||||
--border: #e0e0e0;
|
||||
--shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#status-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
background: var(--card-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-light);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
color: var(--primary);
|
||||
border-bottom: 2px solid var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-with-button input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Volume Control */
|
||||
.volume-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.volume-control input[type="range"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Device List */
|
||||
.device-list {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.device-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.device-address {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.device-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.device-actions .btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.no-devices {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Scan Results */
|
||||
.scan-results {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.scan-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.scan-item:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.scan-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.scan-rssi {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Call Info */
|
||||
#call-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#call-state {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#call-remote {
|
||||
font-size: 1.25rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
#call-duration {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Status Colors */
|
||||
.status-connected {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
header h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* Web API - REST-Endpoints für Konfiguration
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "web_api.h"
|
||||
#include "config/config_manager.h"
|
||||
#include "wifi/wifi_manager.h"
|
||||
#include "bluetooth/bt_manager.h"
|
||||
#include "sip/sip_client.h"
|
||||
#include "audio/audio_router.h"
|
||||
#include "esp_log.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char* TAG = "WEB_API";
|
||||
|
||||
// Hilfsfunktion: JSON-Body aus Request lesen
|
||||
static cJSON* read_json_body(httpd_req_t* req)
|
||||
{
|
||||
int content_len = req->content_len;
|
||||
if (content_len <= 0 || content_len > 4096) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* buf = malloc(content_len + 1);
|
||||
if (!buf) return NULL;
|
||||
|
||||
int received = httpd_req_recv(req, buf, content_len);
|
||||
if (received <= 0) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
|
||||
cJSON* json = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
return json;
|
||||
}
|
||||
|
||||
// Hilfsfunktion: JSON-Antwort senden
|
||||
static esp_err_t send_json_response(httpd_req_t* req, cJSON* json)
|
||||
{
|
||||
char* str = cJSON_PrintUnformatted(json);
|
||||
if (!str) {
|
||||
httpd_resp_send_500(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, str, strlen(str));
|
||||
free(str);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t send_json_error(httpd_req_t* req, int status, const char* message)
|
||||
{
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(json, "success", false);
|
||||
cJSON_AddStringToObject(json, "error", message);
|
||||
|
||||
httpd_resp_set_status(req, status == 400 ? "400 Bad Request" :
|
||||
status == 404 ? "404 Not Found" : "500 Internal Server Error");
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t send_json_success(httpd_req_t* req, const char* message)
|
||||
{
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(json, "success", true);
|
||||
if (message) cJSON_AddStringToObject(json, "message", message);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============ Status API ============
|
||||
|
||||
static esp_err_t api_status_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
|
||||
// WiFi Status
|
||||
cJSON* wifi = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(wifi, "state",
|
||||
wifi_manager_get_state() == WIFI_STATE_STA_CONNECTED ? "connected" :
|
||||
wifi_manager_get_state() == WIFI_STATE_AP_STARTED ? "hotspot" : "disconnected");
|
||||
|
||||
char ip[16] = {0};
|
||||
wifi_manager_get_ip(ip, sizeof(ip));
|
||||
cJSON_AddStringToObject(wifi, "ip", ip);
|
||||
cJSON_AddStringToObject(wifi, "ssid", config->wifi.ssid);
|
||||
cJSON_AddItemToObject(json, "wifi", wifi);
|
||||
|
||||
// SIP Status
|
||||
cJSON* sip = cJSON_CreateObject();
|
||||
sip_reg_state_t reg_state = sip_client_get_reg_state();
|
||||
cJSON_AddStringToObject(sip, "state",
|
||||
reg_state == SIP_REG_STATE_REGISTERED ? "registered" :
|
||||
reg_state == SIP_REG_STATE_REGISTERING ? "registering" : "unregistered");
|
||||
cJSON_AddStringToObject(sip, "server", config->sip.server);
|
||||
cJSON_AddStringToObject(sip, "user", config->sip.username);
|
||||
cJSON_AddItemToObject(json, "sip", sip);
|
||||
|
||||
// Audio Status
|
||||
cJSON* audio = cJSON_CreateObject();
|
||||
audio_source_t source = audio_router_get_active_source();
|
||||
cJSON_AddStringToObject(audio, "source",
|
||||
source == AUDIO_SOURCE_USB ? "usb" :
|
||||
source == AUDIO_SOURCE_BLUETOOTH ? "bluetooth" : "none");
|
||||
cJSON_AddBoolToObject(audio, "usb_connected", audio_router_is_source_available(AUDIO_SOURCE_USB));
|
||||
cJSON_AddBoolToObject(audio, "bt_connected", audio_router_is_source_available(AUDIO_SOURCE_BLUETOOTH));
|
||||
cJSON_AddNumberToObject(audio, "volume", audio_router_get_volume());
|
||||
cJSON_AddBoolToObject(audio, "muted", audio_router_is_muted());
|
||||
cJSON_AddItemToObject(json, "audio", audio);
|
||||
|
||||
// Call Status
|
||||
cJSON* call = cJSON_CreateObject();
|
||||
sip_call_state_t call_state = sip_client_get_call_state();
|
||||
cJSON_AddStringToObject(call, "state",
|
||||
call_state == SIP_CALL_STATE_IDLE ? "idle" :
|
||||
call_state == SIP_CALL_STATE_INCOMING ? "incoming" :
|
||||
call_state == SIP_CALL_STATE_OUTGOING ? "outgoing" :
|
||||
call_state == SIP_CALL_STATE_RINGING ? "ringing" :
|
||||
call_state == SIP_CALL_STATE_CONNECTED ? "connected" : "unknown");
|
||||
|
||||
sip_call_info_t call_info;
|
||||
if (sip_client_get_call_info(&call_info) == ESP_OK && call_state != SIP_CALL_STATE_IDLE) {
|
||||
cJSON_AddStringToObject(call, "remote", call_info.remote_number);
|
||||
cJSON_AddStringToObject(call, "name", call_info.remote_name);
|
||||
cJSON_AddNumberToObject(call, "duration", call_info.duration_sec);
|
||||
}
|
||||
cJSON_AddItemToObject(json, "call", call);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============ WiFi API ============
|
||||
|
||||
static esp_err_t api_wifi_config_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "ssid", config->wifi.ssid);
|
||||
cJSON_AddStringToObject(json, "ip_mode", config->wifi.ip_mode == IP_MODE_DHCP ? "dhcp" : "static");
|
||||
cJSON_AddStringToObject(json, "static_ip", config->wifi.static_ip);
|
||||
cJSON_AddStringToObject(json, "gateway", config->wifi.gateway);
|
||||
cJSON_AddStringToObject(json, "netmask", config->wifi.netmask);
|
||||
cJSON_AddStringToObject(json, "dns", config->wifi.dns);
|
||||
cJSON_AddBoolToObject(json, "configured", config->wifi.configured);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t api_wifi_config_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
wifi_config_data_t wifi_cfg = {0};
|
||||
|
||||
cJSON* ssid = cJSON_GetObjectItem(json, "ssid");
|
||||
cJSON* password = cJSON_GetObjectItem(json, "password");
|
||||
cJSON* ip_mode = cJSON_GetObjectItem(json, "ip_mode");
|
||||
|
||||
if (!ssid || !cJSON_IsString(ssid) || strlen(ssid->valuestring) == 0) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "SSID required");
|
||||
}
|
||||
|
||||
strncpy(wifi_cfg.ssid, ssid->valuestring, CONFIG_MAX_SSID_LEN);
|
||||
if (password && cJSON_IsString(password)) {
|
||||
strncpy(wifi_cfg.password, password->valuestring, CONFIG_MAX_PASSWORD_LEN);
|
||||
}
|
||||
|
||||
wifi_cfg.ip_mode = IP_MODE_DHCP;
|
||||
if (ip_mode && cJSON_IsString(ip_mode) && strcmp(ip_mode->valuestring, "static") == 0) {
|
||||
wifi_cfg.ip_mode = IP_MODE_STATIC;
|
||||
|
||||
cJSON* static_ip = cJSON_GetObjectItem(json, "static_ip");
|
||||
cJSON* gateway = cJSON_GetObjectItem(json, "gateway");
|
||||
cJSON* netmask = cJSON_GetObjectItem(json, "netmask");
|
||||
cJSON* dns = cJSON_GetObjectItem(json, "dns");
|
||||
|
||||
if (static_ip && cJSON_IsString(static_ip))
|
||||
strncpy(wifi_cfg.static_ip, static_ip->valuestring, CONFIG_MAX_IP_LEN);
|
||||
if (gateway && cJSON_IsString(gateway))
|
||||
strncpy(wifi_cfg.gateway, gateway->valuestring, CONFIG_MAX_IP_LEN);
|
||||
if (netmask && cJSON_IsString(netmask))
|
||||
strncpy(wifi_cfg.netmask, netmask->valuestring, CONFIG_MAX_IP_LEN);
|
||||
if (dns && cJSON_IsString(dns))
|
||||
strncpy(wifi_cfg.dns, dns->valuestring, CONFIG_MAX_IP_LEN);
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
esp_err_t err = config_save_wifi(&wifi_cfg);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Save failed");
|
||||
}
|
||||
|
||||
// Neu verbinden
|
||||
wifi_manager_connect(&wifi_cfg);
|
||||
|
||||
return send_json_success(req, "WiFi configuration saved. Connecting...");
|
||||
}
|
||||
|
||||
static esp_err_t api_wifi_scan_get(httpd_req_t* req)
|
||||
{
|
||||
wifi_manager_scan();
|
||||
|
||||
// Warten auf Scan-Ergebnis
|
||||
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||
|
||||
uint16_t num_networks = 0;
|
||||
esp_wifi_scan_get_ap_num(&num_networks);
|
||||
|
||||
if (num_networks > 20) num_networks = 20;
|
||||
|
||||
wifi_ap_record_t* records = malloc(sizeof(wifi_ap_record_t) * num_networks);
|
||||
if (!records) {
|
||||
return send_json_error(req, 500, "Memory error");
|
||||
}
|
||||
|
||||
esp_wifi_scan_get_ap_records(&num_networks, records);
|
||||
|
||||
cJSON* json = cJSON_CreateArray();
|
||||
for (int i = 0; i < num_networks; i++) {
|
||||
cJSON* ap = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(ap, "ssid", (char*)records[i].ssid);
|
||||
cJSON_AddNumberToObject(ap, "rssi", records[i].rssi);
|
||||
cJSON_AddNumberToObject(ap, "channel", records[i].primary);
|
||||
cJSON_AddBoolToObject(ap, "secure", records[i].authmode != WIFI_AUTH_OPEN);
|
||||
cJSON_AddItemToArray(json, ap);
|
||||
}
|
||||
|
||||
free(records);
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// ============ SIP API ============
|
||||
|
||||
static esp_err_t api_sip_config_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "server", config->sip.server);
|
||||
cJSON_AddNumberToObject(json, "port", config->sip.port);
|
||||
cJSON_AddStringToObject(json, "username", config->sip.username);
|
||||
cJSON_AddStringToObject(json, "display_name", config->sip.display_name);
|
||||
cJSON_AddBoolToObject(json, "configured", config->sip.configured);
|
||||
// Passwort wird nicht zurückgegeben
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t api_sip_config_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
sip_config_data_t sip_cfg = {0};
|
||||
sip_cfg.port = CONFIG_BSC_SIP_DEFAULT_PORT;
|
||||
|
||||
cJSON* server = cJSON_GetObjectItem(json, "server");
|
||||
cJSON* port = cJSON_GetObjectItem(json, "port");
|
||||
cJSON* username = cJSON_GetObjectItem(json, "username");
|
||||
cJSON* password = cJSON_GetObjectItem(json, "password");
|
||||
cJSON* display_name = cJSON_GetObjectItem(json, "display_name");
|
||||
|
||||
if (!server || !cJSON_IsString(server) || strlen(server->valuestring) == 0) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Server required");
|
||||
}
|
||||
if (!username || !cJSON_IsString(username) || strlen(username->valuestring) == 0) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Username required");
|
||||
}
|
||||
|
||||
strncpy(sip_cfg.server, server->valuestring, CONFIG_MAX_SIP_SERVER_LEN);
|
||||
strncpy(sip_cfg.username, username->valuestring, CONFIG_MAX_SIP_USER_LEN);
|
||||
|
||||
if (port && cJSON_IsNumber(port)) {
|
||||
sip_cfg.port = (uint16_t)port->valueint;
|
||||
}
|
||||
if (password && cJSON_IsString(password)) {
|
||||
strncpy(sip_cfg.password, password->valuestring, CONFIG_MAX_PASSWORD_LEN);
|
||||
}
|
||||
if (display_name && cJSON_IsString(display_name)) {
|
||||
strncpy(sip_cfg.display_name, display_name->valuestring, CONFIG_MAX_SIP_USER_LEN);
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
esp_err_t err = config_save_sip(&sip_cfg);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Save failed");
|
||||
}
|
||||
|
||||
// Neu registrieren
|
||||
sip_client_unregister();
|
||||
sip_client_register();
|
||||
|
||||
return send_json_success(req, "SIP configuration saved. Registering...");
|
||||
}
|
||||
|
||||
// ============ Bluetooth API ============
|
||||
|
||||
static esp_err_t api_bluetooth_devices_get(httpd_req_t* req)
|
||||
{
|
||||
const device_config_t* config = config_get();
|
||||
|
||||
cJSON* json = cJSON_CreateArray();
|
||||
|
||||
for (int i = 0; i < config->bluetooth.device_count; i++) {
|
||||
const bt_device_config_t* dev = &config->bluetooth.devices[i];
|
||||
cJSON* device = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(device, "address", dev->address);
|
||||
cJSON_AddStringToObject(device, "name", dev->name);
|
||||
cJSON_AddBoolToObject(device, "paired", dev->paired);
|
||||
cJSON_AddBoolToObject(device, "auto_connect", dev->auto_connect);
|
||||
cJSON_AddNumberToObject(device, "priority", dev->priority);
|
||||
// TODO: Check if currently connected
|
||||
cJSON_AddItemToArray(json, device);
|
||||
}
|
||||
|
||||
esp_err_t ret = send_json_response(req, json);
|
||||
cJSON_Delete(json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_scan_post(httpd_req_t* req)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starte Bluetooth-Scan");
|
||||
bt_manager_start_discovery();
|
||||
return send_json_success(req, "Scan started");
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_pair_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* address = cJSON_GetObjectItem(json, "address");
|
||||
if (!address || !cJSON_IsString(address)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Address required");
|
||||
}
|
||||
|
||||
esp_bd_addr_t addr;
|
||||
esp_err_t err = bt_str_to_addr(address->valuestring, addr);
|
||||
if (err != ESP_OK) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Invalid address format");
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
err = bt_manager_pair(addr);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Pairing failed");
|
||||
}
|
||||
|
||||
return send_json_success(req, "Pairing initiated");
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_unpair_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* address = cJSON_GetObjectItem(json, "address");
|
||||
if (!address || !cJSON_IsString(address)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Address required");
|
||||
}
|
||||
|
||||
esp_bd_addr_t addr;
|
||||
esp_err_t err = bt_str_to_addr(address->valuestring, addr);
|
||||
if (err != ESP_OK) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Invalid address format");
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
bt_manager_unpair(addr);
|
||||
config_remove_bt_device(address->valuestring);
|
||||
|
||||
return send_json_success(req, "Device removed");
|
||||
}
|
||||
|
||||
static esp_err_t api_bluetooth_connect_post(httpd_req_t* req)
|
||||
{
|
||||
cJSON* json = read_json_body(req);
|
||||
if (!json) {
|
||||
return send_json_error(req, 400, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* address = cJSON_GetObjectItem(json, "address");
|
||||
if (!address || !cJSON_IsString(address)) {
|
||||
cJSON_Delete(json);
|
||||
return send_json_error(req, 400, "Address required");
|
||||
}
|
||||
|
||||
esp_bd_addr_t addr;
|
||||
esp_err_t err = bt_str_to_addr(address->valuestring, addr);
|
||||
cJSON_Delete(json);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 400, "Invalid address format");
|
||||
}
|
||||
|
||||
err = bt_manager_connect(addr);
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Connection failed");
|
||||
}
|
||||
|
||||
return send_json_success(req, "Connecting...");
|
||||
}
|
||||
|
||||
// ============ Call API ============
|
||||
|
||||
static esp_err_t api_call_answer_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = sip_client_answer();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Answer failed");
|
||||
}
|
||||
return send_json_success(req, "Call answered");
|
||||
}
|
||||
|
||||
static esp_err_t api_call_hangup_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = sip_client_hangup();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Hangup failed");
|
||||
}
|
||||
return send_json_success(req, "Call ended");
|
||||
}
|
||||
|
||||
static esp_err_t api_call_reject_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = sip_client_reject();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Reject failed");
|
||||
}
|
||||
return send_json_success(req, "Call rejected");
|
||||
}
|
||||
|
||||
// ============ System API ============
|
||||
|
||||
static esp_err_t api_system_reboot_post(httpd_req_t* req)
|
||||
{
|
||||
send_json_success(req, "Rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_restart();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t api_system_factory_reset_post(httpd_req_t* req)
|
||||
{
|
||||
esp_err_t err = config_factory_reset();
|
||||
if (err != ESP_OK) {
|
||||
return send_json_error(req, 500, "Reset failed");
|
||||
}
|
||||
send_json_success(req, "Factory reset complete. Rebooting...");
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
esp_restart();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============ Route Registration ============
|
||||
|
||||
void web_api_register_handlers(httpd_handle_t server)
|
||||
{
|
||||
ESP_LOGI(TAG, "Registriere API-Handler");
|
||||
|
||||
// Status
|
||||
httpd_uri_t uri;
|
||||
|
||||
uri = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// WiFi
|
||||
uri = (httpd_uri_t){.uri = "/api/wifi/config", .method = HTTP_GET, .handler = api_wifi_config_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/wifi/config", .method = HTTP_POST, .handler = api_wifi_config_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = api_wifi_scan_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// SIP
|
||||
uri = (httpd_uri_t){.uri = "/api/sip/config", .method = HTTP_GET, .handler = api_sip_config_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/sip/config", .method = HTTP_POST, .handler = api_sip_config_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// Bluetooth
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/devices", .method = HTTP_GET, .handler = api_bluetooth_devices_get};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/scan", .method = HTTP_POST, .handler = api_bluetooth_scan_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/pair", .method = HTTP_POST, .handler = api_bluetooth_pair_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/unpair", .method = HTTP_POST, .handler = api_bluetooth_unpair_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/bluetooth/connect", .method = HTTP_POST, .handler = api_bluetooth_connect_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// Call
|
||||
uri = (httpd_uri_t){.uri = "/api/call/answer", .method = HTTP_POST, .handler = api_call_answer_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/call/hangup", .method = HTTP_POST, .handler = api_call_hangup_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/call/reject", .method = HTTP_POST, .handler = api_call_reject_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
|
||||
// System
|
||||
uri = (httpd_uri_t){.uri = "/api/system/reboot", .method = HTTP_POST, .handler = api_system_reboot_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
uri = (httpd_uri_t){.uri = "/api/system/factory-reset", .method = HTTP_POST, .handler = api_system_factory_reset_post};
|
||||
httpd_register_uri_handler(server, &uri);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Registriert alle API-Handler beim Webserver
|
||||
*/
|
||||
void web_api_register_handlers(httpd_handle_t server);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Web Server - Konfigurations-Weboberfläche
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "web_server.h"
|
||||
#include "web_api.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
static const char* TAG = "WEB_SRV";
|
||||
|
||||
// Embedded Files (aus CMakeLists.txt EMBED_FILES)
|
||||
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
|
||||
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
|
||||
extern const uint8_t style_css_start[] asm("_binary_style_css_start");
|
||||
extern const uint8_t style_css_end[] asm("_binary_style_css_end");
|
||||
extern const uint8_t app_js_start[] asm("_binary_app_js_start");
|
||||
extern const uint8_t app_js_end[] asm("_binary_app_js_end");
|
||||
|
||||
static httpd_handle_t s_server = NULL;
|
||||
|
||||
// Handler für statische Dateien
|
||||
static esp_err_t index_handler(httpd_req_t* req)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_send(req, (const char*)index_html_start, index_html_end - index_html_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t style_handler(httpd_req_t* req)
|
||||
{
|
||||
httpd_resp_set_type(req, "text/css");
|
||||
httpd_resp_send(req, (const char*)style_css_start, style_css_end - style_css_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t js_handler(httpd_req_t* req)
|
||||
{
|
||||
httpd_resp_set_type(req, "application/javascript");
|
||||
httpd_resp_send(req, (const char*)app_js_start, app_js_end - app_js_start);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_init(void)
|
||||
{
|
||||
if (s_server != NULL) {
|
||||
ESP_LOGW(TAG, "Server bereits gestartet");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starte Webserver auf Port %d", CONFIG_BSC_WEB_PORT);
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = CONFIG_BSC_WEB_PORT;
|
||||
config.lru_purge_enable = true;
|
||||
config.max_uri_handlers = 20;
|
||||
config.stack_size = 8192;
|
||||
|
||||
esp_err_t err = httpd_start(&s_server, &config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Server starten fehlgeschlagen: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
// Statische Routen registrieren
|
||||
httpd_uri_t index_uri = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = index_handler,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &index_uri);
|
||||
|
||||
httpd_uri_t style_uri = {
|
||||
.uri = "/style.css",
|
||||
.method = HTTP_GET,
|
||||
.handler = style_handler,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &style_uri);
|
||||
|
||||
httpd_uri_t js_uri = {
|
||||
.uri = "/app.js",
|
||||
.method = HTTP_GET,
|
||||
.handler = js_handler,
|
||||
};
|
||||
httpd_register_uri_handler(s_server, &js_uri);
|
||||
|
||||
// API-Routen registrieren
|
||||
web_api_register_handlers(s_server);
|
||||
|
||||
ESP_LOGI(TAG, "Webserver gestartet");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t web_server_stop(void)
|
||||
{
|
||||
if (s_server == NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Stoppe Webserver");
|
||||
esp_err_t err = httpd_stop(s_server);
|
||||
s_server = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
httpd_handle_t web_server_get_handle(void)
|
||||
{
|
||||
return s_server;
|
||||
}
|
||||
|
||||
esp_err_t web_server_send_ws_event(const char* event_type, const char* json_data)
|
||||
{
|
||||
// WebSocket-Broadcast - wird in einer erweiterten Version implementiert
|
||||
ESP_LOGD(TAG, "WS Event: %s", event_type);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Initialisiert und startet den Webserver
|
||||
*/
|
||||
esp_err_t web_server_init(void);
|
||||
|
||||
/**
|
||||
* Stoppt den Webserver
|
||||
*/
|
||||
esp_err_t web_server_stop(void);
|
||||
|
||||
/**
|
||||
* Gibt das Webserver-Handle zurück
|
||||
*/
|
||||
httpd_handle_t web_server_get_handle(void);
|
||||
|
||||
/**
|
||||
* Sendet ein WebSocket Event an alle verbundenen Clients
|
||||
*/
|
||||
esp_err_t web_server_send_ws_event(const char* event_type, const char* json_data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user