Working
This commit is contained in:
parent
5fba1ce900
commit
7c5ab98553
|
|
@ -2,6 +2,8 @@ from flask import Flask, send_from_directory, session
|
||||||
import os
|
import os
|
||||||
from . import db, config, routes, utils, leds
|
from . import db, config, routes, utils, leds
|
||||||
|
|
||||||
|
leds.get().enter()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Manage secret key
|
# Manage secret key
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import shutil
|
||||||
from . import leds, config
|
from . import leds, config
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
def parse_config(lines):
|
def parse_config(lines):
|
||||||
|
|
@ -62,14 +64,18 @@ class Camera:
|
||||||
|
|
||||||
class RealCamera(Camera):
|
class RealCamera(Camera):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self._entered = False
|
||||||
self.inner = gp.Camera()
|
self.inner = gp.Camera()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.inner.init()
|
if not self._entered:
|
||||||
|
self._entered = True
|
||||||
|
self.inner.init()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.inner.exit()
|
# self.inner.exit()
|
||||||
|
pass
|
||||||
|
|
||||||
def capture(self):
|
def capture(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -79,14 +85,11 @@ class RealCamera(Camera):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def capture_preview(self):
|
def capture_preview(self):
|
||||||
try:
|
capture = gp.check_result(gp.gp_camera_capture_preview(self.inner))
|
||||||
subprocess.run(
|
file_data = gp.check_result(gp.gp_file_get_data_and_size(capture))
|
||||||
"gphoto2 --capture-preview --stdout > src/nenuscanner/static/feed.jpg",
|
data = memoryview(file_data)
|
||||||
shell=True, check=True
|
image = Image.open(io.BytesIO(file_data))
|
||||||
)
|
image.save("src/nenuscanner/static/feed.jpg")
|
||||||
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):
|
def save(self, capture, output_file):
|
||||||
preview = self.inner.file_get(capture.folder, capture.name[:-3] + 'JPG', gp.GP_FILE_TYPE_NORMAL)
|
preview = self.inner.file_get(capture.folder, capture.name[:-3] + 'JPG', gp.GP_FILE_TYPE_NORMAL)
|
||||||
|
|
@ -97,8 +100,17 @@ class RealCamera(Camera):
|
||||||
raw.save(output_file + '.cr2')
|
raw.save(output_file + '.cr2')
|
||||||
|
|
||||||
def config(self):
|
def config(self):
|
||||||
|
|
||||||
|
was_entered = self._entered
|
||||||
|
if self._entered:
|
||||||
|
self._entered = False
|
||||||
|
self.inner.exit()
|
||||||
|
|
||||||
res = subprocess.run(["gphoto2", "--list-all-config"], capture_output=True, encoding="utf-8")
|
res = subprocess.run(["gphoto2", "--list-all-config"], capture_output=True, encoding="utf-8")
|
||||||
|
|
||||||
|
if was_entered:
|
||||||
|
self.__enter__()
|
||||||
|
|
||||||
# print(res.stdout[:200])
|
# print(res.stdout[:200])
|
||||||
|
|
||||||
configs = res.stdout.split("\n")
|
configs = res.stdout.split("\n")
|
||||||
|
|
@ -162,4 +174,4 @@ def set_config(parameter, value):
|
||||||
class CameraException(Exception):
|
class CameraException(Exception):
|
||||||
"""Exception personnalisée pour les erreurs liées à la caméra."""
|
"""Exception personnalisée pour les erreurs liées à la caméra."""
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
|
||||||
|
|
@ -46,19 +46,25 @@ class GpioLed:
|
||||||
|
|
||||||
class GpioLeds(Leds):
|
class GpioLeds(Leds):
|
||||||
def __init__(self, chip: str, gpio_pins: list[int]):
|
def __init__(self, chip: str, gpio_pins: list[int]):
|
||||||
|
self._entered = False
|
||||||
self.chip = gpiod.Chip(chip)
|
self.chip = gpiod.Chip(chip)
|
||||||
self.leds = []
|
self.leds = []
|
||||||
|
|
||||||
for pin in gpio_pins:
|
for pin in gpio_pins:
|
||||||
self.leds.append(GpioLed(pin))
|
self.leds.append(GpioLed(pin))
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
for led in self.leds:
|
if not self._entered:
|
||||||
led.enter(self.chip)
|
self._entered = True
|
||||||
|
for led in self.leds:
|
||||||
|
led.enter(self.chip)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
for led in self.leds:
|
# for led in self.leds:
|
||||||
led.exit()
|
# led.exit()
|
||||||
|
# self._entered = False
|
||||||
|
pass
|
||||||
|
|
||||||
def off(self):
|
def off(self):
|
||||||
for led in self.leds:
|
for led in self.leds:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import subprocess
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
|
|
@ -16,6 +17,21 @@ def index():
|
||||||
projects = db.Object.all_by_project(conn)
|
projects = db.Object.all_by_project(conn)
|
||||||
return render_template('index.html', projects=projects)
|
return render_template('index.html', projects=projects)
|
||||||
|
|
||||||
|
# Route that restarts the server
|
||||||
|
@blueprint.route("/restart")
|
||||||
|
def restart():
|
||||||
|
"""
|
||||||
|
Serves the index of nenuscanner.
|
||||||
|
"""
|
||||||
|
subprocess.Popen(
|
||||||
|
["bash", "-c", "sleep 1 && systemctl restart nenuscanner --user"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
return render_template('restart.html')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
blueprint.register_blueprint(object.blueprint, url_prefix='/object')
|
blueprint.register_blueprint(object.blueprint, url_prefix='/object')
|
||||||
blueprint.register_blueprint(calibration.blueprint, url_prefix='/calibration')
|
blueprint.register_blueprint(calibration.blueprint, url_prefix='/calibration')
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,11 @@ def set_camera_settings():
|
||||||
|
|
||||||
return {'status': 'ok', **updated}
|
return {'status': 'ok', **updated}
|
||||||
|
|
||||||
@blueprint.route('/feed.jpg')
|
@blueprint.route('/feed.jpg', methods=['GET'])
|
||||||
def camera_feed():
|
def camera_feed():
|
||||||
|
capture_preview()
|
||||||
return send_file('static/feed.jpg', mimetype='image/jpeg')
|
return send_file('static/feed.jpg', mimetype='image/jpeg')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/config', methods=['GET'])
|
@blueprint.route('/config', methods=['GET'])
|
||||||
def get_camera_config():
|
def get_camera_config():
|
||||||
"""
|
"""
|
||||||
|
|
@ -96,14 +94,14 @@ def get_camera_config():
|
||||||
|
|
||||||
return jsonify(grouped_params)
|
return jsonify(grouped_params)
|
||||||
|
|
||||||
@blueprint.route('/capture_preview', methods=['POST'])
|
# @blueprint.route('/capture_preview', methods=['POST'])
|
||||||
def capture_preview():
|
def capture_preview():
|
||||||
"""
|
"""
|
||||||
Capture un aperçu avec gphoto2 et sauvegarde dans static/feed.jpg
|
Capture un aperçu avec gphoto2 et sauvegarde dans static/feed.jpg
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cam = C.get()
|
with C.get() as cam:
|
||||||
cam.capture_preview()
|
cam.capture_preview()
|
||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
except C.CameraException as e:
|
except C.CameraException as e:
|
||||||
return jsonify({'status': 'error', 'error': str(e)}), 500
|
return jsonify({'status': 'error', 'error': str(e)}), 500
|
||||||
|
|
|
||||||
|
|
@ -9,36 +9,6 @@ from .. import leds,config
|
||||||
|
|
||||||
blueprint = Blueprint('leds', __name__)
|
blueprint = Blueprint('leds', __name__)
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
# WARNING : how to release
|
|
||||||
|
|
||||||
#@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
|
||||||
|
|
||||||
@blueprint.route('/')
|
@blueprint.route('/')
|
||||||
|
|
@ -61,10 +31,11 @@ def set_led():
|
||||||
led = data.get('led')
|
led = data.get('led')
|
||||||
state = data.get('state')
|
state = data.get('state')
|
||||||
# get the controller (lazy, stored on app.extensions)
|
# get the controller (lazy, stored on app.extensions)
|
||||||
gpio_leds = _get_gpio_leds()
|
gpio_leds = leds.get()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# parse led id/name according to your naming convention
|
# parse led id/name according to your naming convention
|
||||||
|
print([x.gpio_pin for x in gpio_leds.leds])
|
||||||
gpio_led = gpio_leds.get_by_uuid(int(led))
|
gpio_led = gpio_leds.get_by_uuid(int(led))
|
||||||
print(f"Setting {led} / {gpio_led} to {state}")
|
print(f"Setting {led} / {gpio_led} to {state}")
|
||||||
if state == "on":
|
if state == "on":
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="navbarBasicExample" class="navbar-menu">
|
<div id="navbarBasicExample" class="navbar-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="navbar-item" href="/camera">Caméra</a>
|
||||||
|
<a class="navbar-item" href="/leds">Leds</a>
|
||||||
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
|
<a class="navbar-item" href="/restart">Redémarrer le serveur</a>
|
||||||
<a id="calibration-tag-0" class="calibration-tag navbar-item" href="/calibration/calibrate" {% if calibration.state != 0 %}style="display: none;"{% endif %}>
|
<a id="calibration-tag-0" class="calibration-tag navbar-item" href="/calibration/calibrate" {% if calibration.state != 0 %}style="display: none;"{% endif %}>
|
||||||
<span id="calibration-tag-0" class="tags has-addons">
|
<span id="calibration-tag-0" class="tags has-addons">
|
||||||
<span class="tag is-dark">étalonnage</span>
|
<span class="tag is-dark">étalonnage</span>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-half has-text-centered">
|
||||||
<figure class="image is-4by3" style="max-width: 480px; margin: auto;">
|
<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"
|
<img class="preview" id="camera-preview" alt="Camera Preview"
|
||||||
style="border: 1px solid #ccc;">
|
style="border: 1px solid #ccc;">
|
||||||
</figure>
|
</figure>
|
||||||
<button class="button is-small is-info mt-2" type="button" onclick="refreshPreview()">Rafraîchir
|
<button class="button is-small is-info mt-2" type="button" onclick="refreshPreview()">Rafraîchir
|
||||||
|
|
@ -64,13 +64,16 @@
|
||||||
body: JSON.stringify({ [paramName]: value })
|
body: JSON.stringify({ [paramName]: value })
|
||||||
}).then(() => location.reload());
|
}).then(() => location.reload());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const img = document.getElementById('camera-preview');
|
||||||
|
img.onload = function() {
|
||||||
|
requestAnimationFrame(refreshPreview);
|
||||||
|
}
|
||||||
|
|
||||||
window.refreshPreview = function () {
|
function refreshPreview() {
|
||||||
const img = document.getElementById('camera-preview');
|
const url = '/camera/feed.jpg?' + new Date().getTime();
|
||||||
const url = '/static/feed.jpg?' + new Date().getTime();
|
|
||||||
img.src = url;
|
img.src = url;
|
||||||
}
|
}
|
||||||
setInterval(window.refreshPreview, 1000); // Refresh every second
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
fetch('/camera/config')
|
fetch('/camera/config')
|
||||||
|
|
@ -146,7 +149,11 @@
|
||||||
});
|
});
|
||||||
container.appendChild(box);
|
container.appendChild(box);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// When ready, load preview
|
||||||
|
refreshPreview();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
function CapturePreview() {
|
function CapturePreview() {
|
||||||
fetch('/camera/capture_preview', {method: 'POST'})
|
fetch('/camera/capture_preview', {method: 'POST'})
|
||||||
|
|
@ -160,4 +167,4 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock extrajs %}
|
{% endblock extrajs %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue