Compare commits
8 Commits
8d1dff3470
...
13c9bdfd25
| Author | SHA1 | Date |
|---|---|---|
|
|
13c9bdfd25 | |
|
|
e41f0b92d8 | |
|
|
d75427eac1 | |
|
|
9d40f213be | |
|
|
7d9360576f | |
|
|
7ea52bc571 | |
|
|
7eb06bbd57 | |
|
|
5c3bb99d52 |
|
|
@ -184,3 +184,9 @@ poetry.toml
|
|||
pyrightconfig.json
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
|
||||
|
||||
# Custom ignores
|
||||
src/nenuscanner/static/feed.jpg
|
||||
db.sqlite
|
||||
|
||||
|
|
|
|||
44
Readme.md
44
Readme.md
|
|
@ -1,12 +1,48 @@
|
|||
# NenuScanner
|
||||
|
||||
## Lancer l application flask depuis la racine
|
||||
|
||||
|
||||
### Intall local de l applcation
|
||||
|
||||
Avoir un venv
|
||||
|
||||
puis
|
||||
|
||||
```
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
dans src/nenuscanner
|
||||
|
||||
cp config.local.py dans config.py
|
||||
|
||||
|
||||
### Lancer l'application
|
||||
|
||||
```
|
||||
flask --app . run --debug
|
||||
```
|
||||
|
||||
resoudre dependance si besoins genre:
|
||||
|
||||
```
|
||||
pip install gpiod==2.1.3
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## GPIO
|
||||
|
||||
### Cablage des LEDs existantes de la salle noire
|
||||
|
||||
| GPIO | ID LED salle noire |
|
||||
| GPIO 17 | LED 2 |
|
||||
| GPIO 17 | LED 2 |
|
||||
| GPIO 18 | LED 3 |
|
||||
| GPIO 27 | LED 5 |
|
||||
| GPIO 23 | LED 6 |
|
||||
|
|
@ -83,6 +119,12 @@ gphoto2 --set-config iso=1
|
|||
|
||||
## Hardware
|
||||
|
||||
### LED
|
||||
|
||||
LED COB, Cree LED, série CXA2, 90CRI, 2700K Blanc, CXB1512-0000-000N0UK427G 22W
|
||||
|
||||
https://fr.rs-online.com/web/p/led-cob/8847355
|
||||
|
||||
### Connecteur
|
||||
|
||||
VsAT ID
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# python-gphoto2 - Python interface to libgphoto2
|
||||
# http://github.com/jim-easterbrook/python-gphoto2
|
||||
# Copyright (C) 2015-22 Jim Easterbrook jim@jim-easterbrook.me.uk
|
||||
#
|
||||
# This file is part of python-gphoto2.
|
||||
#
|
||||
# python-gphoto2 is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# python-gphoto2 is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with python-gphoto2. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import locale
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import gphoto2 as gp
|
||||
|
||||
def main():
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
logging.basicConfig(
|
||||
format='%(levelname)s: %(name)s: %(message)s', level=logging.WARNING)
|
||||
callback_obj = gp.check_result(gp.use_python_logging())
|
||||
camera = gp.Camera()
|
||||
camera.init()
|
||||
print('Capturing image')
|
||||
file_path = camera.capture(gp.GP_CAPTURE_IMAGE)
|
||||
print('Camera file path: {0}/{1}'.format(file_path.folder, file_path.name))
|
||||
target = os.path.join('/tmp', file_path.name)
|
||||
print('Copying image to', target)
|
||||
camera_file = camera.file_get(
|
||||
file_path.folder, file_path.name, gp.GP_FILE_TYPE_NORMAL)
|
||||
camera_file.save(target)
|
||||
subprocess.call(['xdg-open', target])
|
||||
camera.exit()
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,13 +1,64 @@
|
|||
import subprocess
|
||||
|
||||
from flask import jsonify
|
||||
import gphoto2 as gp
|
||||
import shutil
|
||||
from . import leds, config
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
|
||||
def parse_config(lines):
|
||||
config = {}
|
||||
block = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith('/main/'):
|
||||
# Nouveau bloc : traiter le précédent s'il existe
|
||||
if block:
|
||||
insert_block(config, block)
|
||||
block = []
|
||||
block.append(line)
|
||||
elif line == 'END':
|
||||
block.append(line)
|
||||
insert_block(config, block)
|
||||
block = []
|
||||
else:
|
||||
block.append(line)
|
||||
return config
|
||||
|
||||
|
||||
def insert_block(config, block):
|
||||
path = block[0].strip('/').split('/') # ['main', 'actions', 'syncdatetimeutc']
|
||||
data = {}
|
||||
for line in block[1:-1]: # Exclure le chemin et 'END'
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key.startswith('Choice'):
|
||||
# Extrait les choix dans une liste
|
||||
idx, label = value.split(' ', 1)
|
||||
data.setdefault('Choices', []).append({'id': int(idx), 'label': label})
|
||||
else:
|
||||
data[key] = value
|
||||
|
||||
# Insère dans le dictionnaire hiérarchique
|
||||
d = config
|
||||
for part in path[:-1]: # Traverse les sections, ex: main -> actions
|
||||
d = d.setdefault(part, {})
|
||||
d[path[-1]] = data # Attribue les données à la clé finale
|
||||
|
||||
|
||||
class Camera:
|
||||
def capture(self):
|
||||
return None
|
||||
|
||||
def config(self):
|
||||
return None
|
||||
|
||||
|
||||
class RealCamera(Camera):
|
||||
def __init__(self):
|
||||
|
|
@ -26,6 +77,16 @@ class RealCamera(Camera):
|
|||
except Exception as e:
|
||||
print('An error occured when capturing photo', e)
|
||||
return None
|
||||
|
||||
def capture_preview(self):
|
||||
try:
|
||||
subprocess.run(
|
||||
"gphoto2 --capture-preview --stdout > src/nenuscanner/static/feed.jpg",
|
||||
shell=True, check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print('An error occured when capturing photo', e)
|
||||
raise CameraException(f"Erreur lors de la capture de l'image: {e}")
|
||||
|
||||
def save(self, capture, output_file):
|
||||
preview = self.inner.file_get(capture.folder, capture.name[:-3] + 'JPG', gp.GP_FILE_TYPE_NORMAL)
|
||||
|
|
@ -35,6 +96,23 @@ class RealCamera(Camera):
|
|||
subprocess.run(['convert', output_file + '.jpg', '-resize', '10%', output_file + '.jpg'])
|
||||
raw.save(output_file + '.cr2')
|
||||
|
||||
def config(self):
|
||||
res = subprocess.run(["gphoto2", "--list-all-config"], capture_output=True, encoding="utf-8")
|
||||
|
||||
# print(res.stdout[:200])
|
||||
|
||||
configs = res.stdout.split("\n")
|
||||
print(configs)
|
||||
config_dict = parse_config(configs)
|
||||
|
||||
# Sauvegarde en JSON
|
||||
with open("configCamera.json", "w", encoding="utf-8") as f:
|
||||
json.dump(config_dict, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def set_config(self, parameter, value):
|
||||
subprocess.run(["gphoto2", "--set-config", f"{parameter}={value}"])
|
||||
return 0
|
||||
|
||||
|
||||
class DummyCamera(Camera):
|
||||
def __init__(self, leds: leds.DummyLeds):
|
||||
|
|
@ -73,3 +151,15 @@ camera = DummyCamera(leds.get()) if config.CAMERA == "dummy" else RealCamera()
|
|||
|
||||
def get():
|
||||
return camera
|
||||
|
||||
|
||||
def config():
|
||||
return camera.config()
|
||||
|
||||
def set_config(parameter, value):
|
||||
return camera.set_config(parameter, value)
|
||||
|
||||
class CameraException(Exception):
|
||||
"""Exception personnalisée pour les erreurs liées à la caméra."""
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from flask import Blueprint, render_template
|
||||
|
||||
from .. import db
|
||||
from . import object, calibration, acquisition
|
||||
from . import object, calibration, acquisition, camera, leds
|
||||
|
||||
blueprint = Blueprint('routes', __name__)
|
||||
|
||||
|
|
@ -20,3 +20,5 @@ def index():
|
|||
blueprint.register_blueprint(object.blueprint, url_prefix='/object')
|
||||
blueprint.register_blueprint(calibration.blueprint, url_prefix='/calibration')
|
||||
blueprint.register_blueprint(acquisition.blueprint, url_prefix='/acquisition')
|
||||
blueprint.register_blueprint(camera.blueprint, url_prefix='/camera')
|
||||
blueprint.register_blueprint(leds.blueprint, url_prefix='/leds')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
from flask import Blueprint, render_template, request, send_file, jsonify
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from .. import camera as C
|
||||
|
||||
|
||||
blueprint = Blueprint('camera', __name__)
|
||||
|
||||
# Routes for object management
|
||||
|
||||
|
||||
@blueprint.route('/')
|
||||
def get():
|
||||
"""
|
||||
Returns the page showing camera configuration for all parameters in capturesettings and imgsettings,
|
||||
grouped by section.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
return render_template(
|
||||
'camera.html')
|
||||
|
||||
|
||||
@blueprint.route('/set', methods=['POST'])
|
||||
def set_camera_settings():
|
||||
"""
|
||||
Receives and processes new camera settings for all parameters from the client.
|
||||
"""
|
||||
data = request.get_json()
|
||||
updated = {}
|
||||
for key, value in data.items():
|
||||
print(f"Received {key}: {value}")
|
||||
C.set_config(key, value)
|
||||
updated[key] = value
|
||||
|
||||
try:
|
||||
cam = C.get()
|
||||
cam.capture_preview()
|
||||
return jsonify({'status': 'ok'})
|
||||
except C.CameraException as e:
|
||||
return jsonify({'status': 'error', 'error': str(e)}), 500
|
||||
|
||||
return {'status': 'ok', **updated}
|
||||
|
||||
@blueprint.route('/feed.jpg')
|
||||
def camera_feed():
|
||||
return send_file('static/feed.jpg', mimetype='image/jpeg')
|
||||
|
||||
|
||||
|
||||
|
||||
@blueprint.route('/config', methods=['GET'])
|
||||
def get_camera_config():
|
||||
"""
|
||||
Returns grouped camera parameters as JSON for frontend JS.
|
||||
"""
|
||||
|
||||
try:
|
||||
cam = C.get()
|
||||
cam.config()
|
||||
|
||||
except C.CameraException as e:
|
||||
return jsonify({'status': 'error', 'error': str(e)}), 500
|
||||
|
||||
with open('configCamera.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
grouped_params = []
|
||||
# Iterate over all sections in config['main']
|
||||
for section, settings in config['main'].items():
|
||||
section_params = []
|
||||
if isinstance(settings, dict):
|
||||
for param_name, param in settings.items():
|
||||
if 'Choices' in param and isinstance(param['Choices'], list) and param['Choices']:
|
||||
choices = [
|
||||
{'value': c.get('id', idx), 'label': c['label']}
|
||||
for idx, c in enumerate(param['Choices'])
|
||||
]
|
||||
|
||||
section_params.append({
|
||||
'name': param_name,
|
||||
'label': param.get('Label', param_name.capitalize()),
|
||||
'choices': choices,
|
||||
'current': param.get('Current', ''),
|
||||
'Type': param.get('Type', 'Text'),
|
||||
'Readonly': param.get('Readonly', 0)
|
||||
})
|
||||
|
||||
if section_params:
|
||||
grouped_params.append({
|
||||
'section': section,
|
||||
'params': section_params
|
||||
})
|
||||
|
||||
return jsonify(grouped_params)
|
||||
|
||||
@blueprint.route('/capture_preview', methods=['POST'])
|
||||
def capture_preview():
|
||||
"""
|
||||
Capture un aperçu avec gphoto2 et sauvegarde dans static/feed.jpg
|
||||
"""
|
||||
try:
|
||||
cam = C.get()
|
||||
cam.capture_preview()
|
||||
return jsonify({'status': 'ok'})
|
||||
except C.CameraException as e:
|
||||
return jsonify({'status': 'error', 'error': str(e)}), 500
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from flask import Blueprint, render_template, request, send_file, jsonify
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from .. import camera as C
|
||||
from .. import leds
|
||||
|
||||
|
||||
blueprint = Blueprint('leds', __name__)
|
||||
|
||||
# Routes for object management
|
||||
|
||||
|
||||
@blueprint.route('/')
|
||||
def get():
|
||||
"""
|
||||
Returns the pages showing all leds.
|
||||
"""
|
||||
|
||||
return render_template(
|
||||
'leds.html')
|
||||
|
||||
@blueprint.route('/set', methods=['POST'])
|
||||
def set_led():
|
||||
"""
|
||||
Reçoit une commande pour allumer ou éteindre une LED.
|
||||
Attend un JSON : { "led": "led1", "state": "on" } ou { "led": "led2", "state": "off" }
|
||||
"""
|
||||
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":
|
||||
gpio_led.on()
|
||||
else:
|
||||
gpio_led.off()
|
||||
|
||||
|
||||
print(f"Commande reçue pour {led} : {state}")
|
||||
|
||||
return jsonify({'status': 'ok', 'led': led, 'state': state})
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
.cameraSettings {
|
||||
|
||||
height: 800px;
|
||||
border: 1px solid;
|
||||
overflow: scroll;
|
||||
scrollbar-color: red orange;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
img#preview {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
|
||||
.switch-slice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
.switch-slice label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.switch-slice .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
.switch-slice .switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.switch-slice .slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
.switch-slice .slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.switch-slice input:checked + .slider {
|
||||
background-color: #48c774;
|
||||
}
|
||||
.switch-slice input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NenuScanner</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/bulma.min.css">
|
||||
<link rel="stylesheet" href="/static/custom.css">
|
||||
{% block extracss %}{% endblock extracss %}
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Configurer la camera</h1>
|
||||
<form id="camera-config-form">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="cameraSettings" id="camera-config-container"></div>
|
||||
</div>
|
||||
<div class="column is-half has-text-centered">
|
||||
<figure class="image is-4by3" style="max-width: 480px; margin: auto;">
|
||||
<img class="preview" id="camera-preview" src="/static/feed.jpg?{{ range(1000000)|random }}" alt="Camera Preview"
|
||||
style="border: 1px solid #ccc;">
|
||||
</figure>
|
||||
<button class="button is-small is-info mt-2" type="button" onclick="refreshPreview()">Rafraîchir
|
||||
l’aperçu
|
||||
</button>
|
||||
<button class="button is-small is-info mt-2" type="button" onclick="CapturePreview()">Acquistion camera
|
||||
|
||||
</button>
|
||||
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Speed</p>
|
||||
<p id="display-camera-shutterspeed" class="title is-5">Updating ...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">f/</p>
|
||||
<p id="display-camera-aperture" class="title is-5"> Updating ...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">ISO</p>
|
||||
<p id="display-camera-iso" class="title is-5">Updating ...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">°K</p>
|
||||
<p id="display-camera-color-temperature" class="title is-5">Updating ...</p>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
function setConfig(paramName) {
|
||||
const value = document.getElementById(paramName).value;
|
||||
fetch('/camera/set', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [paramName]: value })
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
|
||||
window.refreshPreview = function () {
|
||||
const img = document.getElementById('camera-preview');
|
||||
const url = '/static/feed.jpg?' + new Date().getTime();
|
||||
img.src = url;
|
||||
}
|
||||
setInterval(window.refreshPreview, 1000); // Refresh every second
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
fetch('/camera/config')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
// data is grouped_params
|
||||
const container = document.getElementById('camera-config-container');
|
||||
container.innerHTML = '';
|
||||
const captureSection = data.find(group => group.section === 'capturesettings');
|
||||
if (captureSection) {
|
||||
// Find the shutterspeed param
|
||||
const shutterParam = captureSection.params.find(param => param.name === 'shutterspeed');
|
||||
if (shutterParam) {
|
||||
document.getElementById('display-camera-shutterspeed').textContent = shutterParam.current;
|
||||
}
|
||||
// find the aperture param
|
||||
const apertureParam = captureSection.params.find(param => param.name === 'aperture');
|
||||
if (apertureParam) {
|
||||
document.getElementById('display-camera-aperture').textContent = apertureParam.current;
|
||||
}
|
||||
}
|
||||
|
||||
const imgSection = data.find(group => group.section === 'imgsettings');
|
||||
if (imgSection) {
|
||||
// Find the iso param
|
||||
const isoParam = imgSection.params.find(param => param.name === 'iso');
|
||||
if (isoParam) {
|
||||
document.getElementById('display-camera-iso').textContent = isoParam.current;
|
||||
}
|
||||
// find the whitebalance param
|
||||
const wbParam = imgSection.params.find(param => param.name === 'colortemperature');
|
||||
if (wbParam) {
|
||||
document.getElementById('display-camera-color-temperature').textContent = wbParam.current;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data.forEach(group => {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'box';
|
||||
box.innerHTML = `<h2 class="subtitle has-text-weight-bold">${group.section.replace('settings', ' Settings')}</h2>`;
|
||||
group.params.forEach(param => {
|
||||
let field = document.createElement('div');
|
||||
if (param.type == 'TEXT') {
|
||||
field.className = 'text';
|
||||
field.innerHTML = `
|
||||
<p> ${param.name} </p>
|
||||
`;
|
||||
|
||||
}else {
|
||||
field.className = 'field';
|
||||
field.innerHTML = `
|
||||
<label class="label" for="${param.name}">${param.label}:</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="${param.name}" name="${param.name}">
|
||||
${param.choices.map(choice =>
|
||||
`<option value="${choice.value}" ${choice.label === param.current ? 'selected' : ''}>
|
||||
${choice.label} (${choice.value})
|
||||
</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control mt-2">
|
||||
<button class="button is-small is-primary" type="button" onclick="setConfig('${param.name}')">Set</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
box.appendChild(field);
|
||||
|
||||
});
|
||||
container.appendChild(box);
|
||||
});
|
||||
});
|
||||
});
|
||||
function CapturePreview() {
|
||||
fetch('/camera/capture_preview', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'ok') {
|
||||
refreshPreview(); // Rafraîchir l'image après la capture
|
||||
} else {
|
||||
alert('Erreur lors de la capture : ' + (data.error || 'inconnue'));
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% 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="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>
|
||||
</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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exemple d’attachement 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>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Strame ok mais stream imge fixe
|
||||
#gphoto2 --stdout --capture-movie | ffmpeg -i - -c:v libx264 -f hls -hls_time 2 -hls_list_size 3 -hls_flags delete_segments src/nenuscanner/static/stream.m3u8
|
||||
|
||||
|
||||
gphoto2 --capture-image-and-download -F 0 -I 2 --stdout | ffmpeg -f image2pipe -framerate 0.5 -i - -r 25 -c:v libx264 -f hls -hls_time 2 -hls_list_size 3 -hls_flags delete_segments -g 50 -keyint_min 50 src/nenuscanner/static/stream.m3u8
|
||||
Loading…
Reference in New Issue