444 lines
16 KiB
JavaScript
444 lines
16 KiB
JavaScript
// 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();
|
|
}
|
|
})();
|