Updates Leds pages

This commit is contained in:
Nicolas Bertrand 2025-11-05 10:29:35 +01:00
parent 13c9bdfd25
commit 84a51af78d
3 changed files with 205 additions and 76 deletions

View File

@ -15,6 +15,8 @@ class Leds:
def off(self): def off(self):
pass pass
class GpioLed: class GpioLed:
@ -65,7 +67,18 @@ class GpioLeds(Leds):
def on(self): def on(self):
for led in self.leds: for led in self.leds:
led.on() led.on()
def enter(self):
return self.__enter__()
def exit(self,*args):
self.__exit__(*args)
def get_by_uuid(self, uuid: int) -> GpioLed:
for led in self.leds:
if led.gpio_pin == uuid:
return led
raise ValueError(f"No LED with UUID {uuid}")
class DummyLed: class DummyLed:
def __init__(self, gpio_pin: int): def __init__(self, gpio_pin: int):
@ -110,10 +123,22 @@ class DummyLeds(Leds):
def on(self): def on(self):
for led in self.leds: for led in self.leds:
led.on() led.on()
def enter(self):
return self.__enter__()
def exit(self,*args):
self.__exit__(*args)
def get_by_uuid(self, uuid: int) -> DummyLed:
for led in self.leds:
if led.gpio_pin == uuid:
return led
raise ValueError(f"No LED with UUID {uuid}")
_leds = GpioLeds(config.GPIO_CHIP, config.LEDS_UUIDS) if config.GPIO_CHIP is not None else DummyLeds(config.LEDS_UUIDS) _leds = GpioLeds(config.GPIO_CHIP, config.LEDS_UUIDS) if config.GPIO_CHIP is not None else DummyLeds(config.LEDS_UUIDS)
def get() -> Leds: def get() -> Leds:
return _leds return _leds

View File

@ -1,13 +1,45 @@
from flask import Blueprint, render_template, request, send_file, jsonify from flask import Blueprint, render_template, request, send_file, jsonify, session, current_app
import json import json
import subprocess import subprocess
from .. import camera as C from .. import camera as C
from .. import leds from .. import leds,config
blueprint = Blueprint('leds', __name__) blueprint = Blueprint('leds', __name__)
# WARNING: This is a temporary global variable to hold the state of the GPIO LEDs.
# This is necessary because the LED state must persist across multiple requests,
# and Flask does not maintain state between requests.
# A better solution would be to implement a proper state management system.
def _get_gpio_leds():
"""Return a singleton leds controller stored in app.extensions."""
app = current_app._get_current_object()
ext_key = 'nenuscanner_gpio_leds'
if ext_key not in app.extensions:
# create and store the resource
app.extensions[ext_key] = leds.get().enter()
return app.extensions[ext_key]
@blueprint.record_once
def _register_cleanup(state):
"""
Register a teardown handler on the app that will try to exit the leds controller
when the app context is torn down.
"""
app = state.app
ext_key = 'nenuscanner_gpio_leds'
@app.teardown_appcontext
def _cleanup(exception=None):
gpio = app.extensions.get(ext_key)
if gpio:
try:
gpio.exit()
except Exception:
# best effort cleanup, don't raise during teardown
pass
# Routes for object management # Routes for object management
@ -16,9 +48,15 @@ def get():
""" """
Returns the pages showing all leds. Returns the pages showing all leds.
""" """
gpio_leds = _get_gpio_leds()
print(gpio_leds)
for i, led in enumerate(gpio_leds.leds):
print(f"LED {i}: {led}, is_on={led.is_on}")
return render_template( return render_template(
'leds.html') 'leds.html', leds= config.LEDS_UUIDS)
@blueprint.route('/set', methods=['POST']) @blueprint.route('/set', methods=['POST'])
def set_led(): def set_led():
@ -29,17 +67,18 @@ def set_led():
data = request.get_json() data = request.get_json()
led = data.get('led') led = data.get('led')
state = data.get('state') state = data.get('state')
ledId=int(led[3]) # get the controller (lazy, stored on app.extensions)
gpio_leds = _get_gpio_leds()
with leds.get() as gpio_leds:
print(gpio_leds.leds) try:
gpio_led=gpio_leds.leds[ledId] # parse led id/name according to your naming convention
if state == "on": gpio_led = gpio_leds.get_by_uuid(int(led))
print(f"Setting {led} / {gpio_led} to {state}")
if state == "on":
gpio_led.on() gpio_led.on()
else: else:
gpio_led.off() gpio_led.off()
except Exception as e:
return jsonify({'status': 'error', 'error': str(e)}), 400
print(f"Commande reçue pour {led} : {state}")
return jsonify({'status': 'ok', 'led': led, 'state': state}) return jsonify({'status': 'ok', 'led': led, 'state': state})

View File

@ -2,73 +2,138 @@
{% block content %} {% block content %}
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<h1 class="title">Conroler les LEDS</h1> <h1 class="title">Contrôler les LEDS</h1>
<div class="columns">
<div class="column is-half">
<div class="switch-slice"> <div id="leds-container" class="columns is-multiline">
<h2>LED 1</h2> <!-- Les switches seront créés à la volée par JS à partir de la variable `leds` -->
<label for="led-switch">OFF</label> </div>
<label class="switch"> </div>
<input type="checkbox" id="led-switch-led1">
<span class="slider"></span>
</label>
<label for="led-switch">ON</label>
</div>
<div class="switch-slice">
<h2>LED 2</h2>
<label for="led-switch">OFF</label>
<label class="switch">
<input type="checkbox" id="led-switch-led2">
<span class="slider"></span>
</label>
<label for="led-switch">ON</label>
</div>
<div class="switch-slice">
<h2>LED 3</h2>
<label for="led-switch">OFF</label>
<label class="switch">
<input type="checkbox" id="led-switch-led3">
<span class="slider"></span>
</label>
<label for="led-switch">ON</label>
</div>
<div class="column">
</div>
</div>
</section> </section>
{% endblock content %} {% endblock content %}
{% block extrajs %} {% block extrajs %}
<script> <style>
function setLedState(ledName, state) { /* switch simple */
fetch('/leds/set', { .switch-slice {
method: 'POST', display:flex;
headers: { 'Content-Type': 'application/json' }, align-items:center;
body: JSON.stringify({ led: ledName, state: state }) gap:0.5rem;
}) }
.then(response => response.json()) .switch {
.then(data => { position: relative;
if (data.status !== 'ok') { display: inline-block;
alert('Erreur lors du contrôle de la LED : ' + (data.error || 'inconnue')); width: 46px;
} height: 24px;
}); }
} .switch input { display: none; }
.slider {
position:absolute; inset:0; cursor:pointer;
background:#ddd; border-radius:24px; transition:.2s;
}
.slider:before {
content:""; position:absolute; height:18px; width:18px; left:3px; top:3px;
background:#fff; border-radius:50%; transition:.2s;
}
.switch input:checked + .slider { background:#48c774; }
.switch input:checked + .slider:before { transform: translateX(22px); }
/* remove fixed width so Bulma columns control layout */
.led-box { padding:0.75rem; max-width: 100%; }
.led-name { font-weight:600; margin-bottom:.35rem; }
</style>
// Exemple dattachement dévénement pour plusieurs LEDs <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
['led1', 'led2', 'led3'].forEach(function (ledName) { // leds list injected by Jinja
const checkbox = document.getElementById('led-switch-' + ledName); const leds = {{ leds | tojson | safe }};
if (checkbox) {
checkbox.addEventListener('change', function () { const container = document.getElementById('leds-container');
setLedState(ledName, checkbox.checked ? 'on' : 'off');
}); // helper: send state to server
} async function setLedState(ledName, state, checkbox) {
}); try {
const res = await fetch('/leds/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ led: ledName, state: state })
});
const json = await res.json();
if (res.ok && json.status === 'ok') {
checkbox.classList.remove('has-text-danger');
checkbox.classList.add('has-text-success');
setTimeout(()=>{ checkbox.classList.remove('has-text-success'); }, 500);
} else {
console.error('LED set error', json);
alert('Erreur commande LED: ' + (json.error || res.status));
}
} catch (e) {
console.error(e);
alert('Erreur réseau lors de la commande LED');
}
}
// create a switch card for each led name/uuid
leds.forEach(function(ledName, idx) {
// Use Bulma column classes to get max 3 per row on desktop
const col = document.createElement('div');
col.className = 'column is-one-third-desktop is-half-tablet is-full-mobile led-box';
const box = document.createElement('div');
const label = document.createElement('div');
label.className = 'led-name';
label.textContent = ledName;
box.appendChild(label);
const switchRow = document.createElement('div');
switchRow.className = 'switch-slice';
const offLabel = document.createElement('span');
offLabel.textContent = 'OFF';
offLabel.style.fontSize = '0.9rem';
const switchLabel = document.createElement('label');
switchLabel.className = 'switch';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = 'led-switch-' + idx;
input.dataset.led = ledName;
const slider = document.createElement('span');
slider.className = 'slider';
switchLabel.appendChild(input);
switchLabel.appendChild(slider);
const onLabel = document.createElement('span');
onLabel.textContent = 'ON';
onLabel.style.fontSize = '0.9rem';
switchRow.appendChild(offLabel);
switchRow.appendChild(switchLabel);
switchRow.appendChild(onLabel);
box.appendChild(switchRow);
// Optional small status text
const status = document.createElement('div');
status.style.marginTop = '0.5rem';
status.style.fontSize = '0.85rem';
status.textContent = 'état: inconnu';
box.appendChild(status);
col.appendChild(box);
container.appendChild(col);
// event
input.addEventListener('change', function (ev) {
const led = ev.target.dataset.led;
const state = ev.target.checked ? 'on' : 'off';
status.textContent = 'état: ' + state;
setLedState(led, state, status);
}); });
});
});
</script> </script>
{% endblock extrajs %} {% endblock extrajs %}