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):
pass
class GpioLed:
@ -65,7 +67,18 @@ class GpioLeds(Leds):
def on(self):
for led in self.leds:
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:
def __init__(self, gpio_pin: int):
@ -110,10 +123,22 @@ class DummyLeds(Leds):
def on(self):
for led in self.leds:
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)
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 subprocess
from .. import camera as C
from .. import leds
from .. import leds,config
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
@ -16,9 +48,15 @@ def get():
"""
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(
'leds.html')
'leds.html', leds= config.LEDS_UUIDS)
@blueprint.route('/set', methods=['POST'])
def set_led():
@ -29,17 +67,18 @@ def set_led():
data = request.get_json()
led = data.get('led')
state = data.get('state')
ledId=int(led[3])
with leds.get() as gpio_leds:
print(gpio_leds.leds)
gpio_led=gpio_leds.leds[ledId]
if state == "on":
# get the controller (lazy, stored on app.extensions)
gpio_leds = _get_gpio_leds()
try:
# parse led id/name according to your naming convention
gpio_led = gpio_leds.get_by_uuid(int(led))
print(f"Setting {led} / {gpio_led} to {state}")
if state == "on":
gpio_led.on()
else:
else:
gpio_led.off()
print(f"Commande reçue pour {led} : {state}")
except Exception as e:
return jsonify({'status': 'error', 'error': str(e)}), 400
return jsonify({'status': 'ok', 'led': led, 'state': state})

View File

@ -2,73 +2,138 @@
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">Conroler les LEDS</h1>
<div class="columns">
<div class="column is-half">
<div class="container">
<h1 class="title">Contrôler les LEDS</h1>
<div class="switch-slice">
<h2>LED 1</h2>
<label for="led-switch">OFF</label>
<label class="switch">
<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>
<div id="leds-container" class="columns is-multiline">
<!-- Les switches seront créés à la volée par JS à partir de la variable `leds` -->
</div>
</div>
</section>
{% endblock content %}
{% block extrajs %}
<script>
function setLedState(ledName, state) {
fetch('/leds/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ led: ledName, state: state })
})
.then(response => response.json())
.then(data => {
if (data.status !== 'ok') {
alert('Erreur lors du contrôle de la LED : ' + (data.error || 'inconnue'));
}
});
}
<style>
/* switch simple */
.switch-slice {
display:flex;
align-items:center;
gap:0.5rem;
}
.switch {
position: relative;
display: inline-block;
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
document.addEventListener('DOMContentLoaded', function () {
['led1', 'led2', 'led3'].forEach(function (ledName) {
const checkbox = document.getElementById('led-switch-' + ledName);
if (checkbox) {
checkbox.addEventListener('change', function () {
setLedState(ledName, checkbox.checked ? 'on' : 'off');
});
}
});
<script>
document.addEventListener('DOMContentLoaded', function () {
// leds list injected by Jinja
const leds = {{ leds | tojson | safe }};
const container = document.getElementById('leds-container');
// 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>
{% endblock extrajs %}