next step - added control.html and functions

This commit is contained in:
oberon 2026-02-03 16:03:05 +01:00
parent 8955cff47b
commit 127754ba15
3 changed files with 250 additions and 15 deletions

62
app.py
View File

@ -47,35 +47,69 @@ def index():
return render_template('index.html', configs=configs) return render_template('index.html', configs=configs)
current_config = None
current_filename = None
@app.route('/load_config/<filename>') @app.route('/load_config/<filename>')
def load_config(filename): def load_config(filename):
# Später: Config in Session speichern oder global (für Entwicklung erstmal redirect) global current_config, current_filename
# Hier nur redirect zur Steuerseite echte Logik kommt später
path = os.path.join(app.config['CONFIG_DIR'], filename) path = os.path.join(app.config['CONFIG_DIR'], filename)
if not os.path.exists(path): if not os.path.exists(path):
return "Konfigurationsdatei nicht gefunden", 404 return "Konfigurationsdatei nicht gefunden", 404
# Für den Moment nur redirect später speichern wir current_config try:
return redirect(url_for('control_page', filename=filename)) with open(path, 'r', encoding='utf-8') as f:
current_config = json.load(f)
current_config['filename'] = filename
current_filename = filename
logger.info(f"Konfiguration geladen: {filename}")
return redirect(url_for('control_page'))
except Exception as e:
logger.error(f"Fehler beim Laden der Config {filename}: {e}")
return "Fehler beim Laden der Konfiguration", 500
@app.route('/control/<filename>') @app.route('/control')
def control_page(filename): def control_page():
# Später: Config laden und übergeben if current_config is None:
# Aktuell nur Dummy-Template return redirect(url_for('index'))
return render_template('control.html', filename=filename)
return render_template('control.html', config=current_config)
@app.route('/configs/<path:filename>') # Neue API-Route (Platzhalter später echte Bluetooth-Steuerung)
def serve_config_file(filename): @app.route('/api/connect', methods=['POST'])
"""Bilder und andere Dateien aus configs/ ausliefern""" def api_connect():
return send_from_directory(app.config['CONFIG_DIR'], filename) # Hier kommt später der echte Code mit MouldKing
# Aktuell nur simuliert
if current_config is None:
return jsonify({"success": False, "message": "Keine Konfiguration geladen"}), 400
logger.info(f"Simulierter Connect zu Hub {current_config.get('hub_id')}")
return jsonify({
"success": True,
"message": "Verbunden (Simulation)",
"hub_id": current_config.get('hub_id')
})
@app.route('/api/control', methods=['POST'])
def api_control():
# Platzhalter für spätere echte Steuerung
data = request.get_json()
logger.info(f"Steuerbefehl empfangen: {data}")
return jsonify({"success": True, "message": "Befehl gesendet (Simulation)"})
@app.route('/api/stop_all', methods=['POST'])
def api_stop_all():
logger.info("Alle stoppen (Simulation)")
return jsonify({"success": True, "message": "Alle Kanäle gestoppt"})
@app.route('/admin') @app.route('/admin')
def admin(): def admin():
return render_template('admin.html') return render_template('admin.html')
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5050, debug=True) app.run(host='0.0.0.0', port=5055, debug=True)

View File

@ -3,5 +3,139 @@ console.log("MK Control JS geladen");
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('MK Control Frontend geladen'); console.log('MK Control Frontend geladen');
// Hier kommen später alle Fetch-Logiken, Slider-Handler etc. rein
const connectBtn = document.getElementById('connect-btn');
const connectSection = document.getElementById('connect-section');
const controlSection = document.getElementById('control-section');
const channelsContainer = document.getElementById('channels-container');
const stopAllBtn = document.getElementById('stop-all-btn');
if (!connectBtn) return; // Sicherstellen, dass wir auf der richtigen Seite sind
connectBtn.addEventListener('click', async () => {
connectBtn.disabled = true;
connectBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> Verbinde...';
try {
const response = await fetch('/api/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (result.success) {
connectSection.style.display = 'none';
controlSection.style.display = 'block';
renderChannels();
alert('Verbunden! (Simulation)');
} else {
alert('Verbindung fehlgeschlagen: ' + (result.message || 'Unbekannter Fehler'));
}
} catch (err) {
console.error(err);
alert('Fehler bei der Verbindung: ' + err.message);
} finally {
connectBtn.disabled = false;
connectBtn.innerHTML = '<i class="bi bi-bluetooth me-2"></i> Mit Hub verbinden';
}
});
stopAllBtn.addEventListener('click', async () => {
if (!confirm('Wirklich alle Kanäle stoppen?')) return;
try {
const res = await fetch('/api/stop_all', { method: 'POST' });
const data = await res.json();
if (data.success) {
alert('Alle Kanäle gestoppt');
}
} catch (err) {
alert('Fehler beim Stoppen');
}
});
function renderChannels() {
channelsContainer.innerHTML = '';
if (!config.channels || config.channels.length === 0) {
channelsContainer.innerHTML = '<p class="text-center text-muted py-4">Keine Kanäle in der Konfiguration definiert.</p>';
return;
}
config.channels.forEach(channel => {
const col = document.createElement('div');
col.className = 'col-md-6 mb-4';
let controlHTML = '';
if (channel.type === 'motor') {
controlHTML = `
<label class="form-label">${channel.name} (${channel.port})</label>
<input type="range" class="form-range motor-slider"
min="-100" max="100" value="0" step="5"
data-port="${channel.port}">
<div class="text-center mt-1">
<span class="value-display">0 %</span>
</div>
`;
} else {
// Licht, Sound, Fogger → Toggle-Button
controlHTML = `
<label class="form-label">${channel.name} (${channel.port})</label>
<button class="btn btn-outline-secondary w-100 toggle-btn"
data-port="${channel.port}" data-state="off">
AUS
</button>
`;
}
col.innerHTML = `
<div class="card h-100">
<div class="card-body">
${controlHTML}
</div>
</div>
`;
channelsContainer.appendChild(col);
});
// Event-Listener für Slider
document.querySelectorAll('.motor-slider').forEach(slider => {
const display = slider.parentElement.querySelector('.value-display');
slider.addEventListener('input', async () => {
const value = parseInt(slider.value) / 100;
display.textContent = `${slider.value} %`;
await sendControl(slider.dataset.port, value);
});
});
// Event-Listener für Toggle-Buttons
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const current = btn.dataset.state;
const newState = current === 'off' ? 'on' : 'off';
btn.dataset.state = newState;
btn.textContent = newState === 'on' ? 'EIN' : 'AUS';
btn.classList.toggle('btn-success', newState === 'on');
btn.classList.toggle('btn-outline-secondary', newState === 'off');
await sendControl(btn.dataset.port, newState === 'on' ? 1 : 0);
});
});
}
async function sendControl(port, value) {
try {
const res = await fetch('/api/control', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port, value })
});
// Kann später Feedback geben
} catch (err) {
console.error('Steuerbefehl fehlgeschlagen:', err);
}
}
}); });

View File

@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %}{{ config.name }} Steuerung{% endblock %}
{% block content %}
<div class="container my-4">
<div class="row mb-4 align-items-center">
<div class="col-md-8">
<h1 class="mb-1">{{ config.name }}</h1>
<p class="text-muted">
Hub-ID: {{ config.hub_id | default('unbekannt') }} •
Typ: {{ config.hub_type | default('unbekannt') }} •
Datei: {{ config.filename }}
</p>
</div>
<div class="col-md-4 text-md-end">
{% if config.image %}
<img src="{{ url_for('serve_config_file', filename=config.image) }}"
alt="{{ config.name }}"
class="img-fluid rounded shadow"
style="max-height: 180px; object-fit: cover;">
{% else %}
<div class="bg-light rounded d-flex align-items-center justify-content-center shadow"
style="height: 180px; width: 100%; max-width: 300px; margin-left: auto;">
<i class="bi bi-train-freight-front display-1 text-muted"></i>
</div>
{% endif %}
</div>
</div>
<!-- Connect-Button -->
<div class="text-center mb-5" id="connect-section">
<button id="connect-btn" class="btn btn-lg btn-success px-5 py-3">
<i class="bi bi-bluetooth me-2"></i> Mit Hub verbinden
</button>
<p class="mt-3 text-muted small">Stelle sicher, dass der Hub eingeschaltet ist und im Bluetooth-Modus ausgewählt wurde.</p>
</div>
<!-- Steuerbereich anfangs versteckt -->
<div id="control-section" style="display: none;">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Steuerung</h5>
</div>
<div class="card-body" id="channels-container">
<!-- Dynamische Kanäle werden hier per JavaScript eingefügt -->
</div>
<div class="card-footer text-center">
<button id="stop-all-btn" class="btn btn-danger btn-lg px-5">
<i class="bi bi-stop-fill me-2"></i> Alle stoppen
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Config direkt ins JS übergeben
const config = {{ config | tojson | safe }};
</script>
{% endblock %}