WIP
This commit is contained in:
parent
5c3bb99d52
commit
7eb06bbd57
36
Readme.md
36
Readme.md
|
|
@ -1,5 +1,41 @@
|
||||||
# NenuScanner
|
# 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
|
## GPIO
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -2,12 +2,61 @@ import subprocess
|
||||||
import gphoto2 as gp
|
import gphoto2 as gp
|
||||||
import shutil
|
import shutil
|
||||||
from . import leds, config
|
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:
|
class Camera:
|
||||||
def capture(self):
|
def capture(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def config(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class RealCamera(Camera):
|
class RealCamera(Camera):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -35,6 +84,23 @@ class RealCamera(Camera):
|
||||||
subprocess.run(['convert', output_file + '.jpg', '-resize', '10%', output_file + '.jpg'])
|
subprocess.run(['convert', output_file + '.jpg', '-resize', '10%', output_file + '.jpg'])
|
||||||
raw.save(output_file + '.cr2')
|
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):
|
class DummyCamera(Camera):
|
||||||
def __init__(self, leds: leds.DummyLeds):
|
def __init__(self, leds: leds.DummyLeds):
|
||||||
|
|
@ -73,3 +139,10 @@ camera = DummyCamera(leds.get()) if config.CAMERA == "dummy" else RealCamera()
|
||||||
|
|
||||||
def get():
|
def get():
|
||||||
return camera
|
return camera
|
||||||
|
|
||||||
|
|
||||||
|
def config():
|
||||||
|
return camera.config()
|
||||||
|
|
||||||
|
def set_config(parameter, value):
|
||||||
|
return camera.set_config(parameter, value)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
from . import object, calibration, acquisition
|
from . import object, calibration, acquisition, camera
|
||||||
|
|
||||||
blueprint = Blueprint('routes', __name__)
|
blueprint = Blueprint('routes', __name__)
|
||||||
|
|
||||||
|
|
@ -20,3 +20,4 @@ def index():
|
||||||
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')
|
||||||
blueprint.register_blueprint(acquisition.blueprint, url_prefix='/acquisition')
|
blueprint.register_blueprint(acquisition.blueprint, url_prefix='/acquisition')
|
||||||
|
blueprint.register_blueprint(camera.blueprint, url_prefix='/camera')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
from flask import Blueprint, render_template, request, redirect
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .. import camera as C
|
||||||
|
|
||||||
|
blueprint = Blueprint('camera', __name__)
|
||||||
|
|
||||||
|
# Routes for object management
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/')
|
||||||
|
def get():
|
||||||
|
"""
|
||||||
|
Returns the page showing camera configuration
|
||||||
|
"""
|
||||||
|
# Load configCamera.json
|
||||||
|
with open('configCamera.json', 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Extract shutterspeed choices and current value
|
||||||
|
shutterspeed = config['main']['capturesettings']['shutterspeed']
|
||||||
|
shutterspeed_choices = [
|
||||||
|
{'value': c.get('id', idx), 'label': c['label']}
|
||||||
|
for idx, c in enumerate(shutterspeed['Choices'])
|
||||||
|
]
|
||||||
|
shutterspeed_current = shutterspeed['Current']
|
||||||
|
|
||||||
|
# Extract aperture choices and current value
|
||||||
|
aperture = config['main']['capturesettings']['aperture']
|
||||||
|
aperture_choices = [
|
||||||
|
{'value': c.get('id', idx), 'label': c['label']}
|
||||||
|
for idx, c in enumerate(aperture['Choices'])
|
||||||
|
]
|
||||||
|
aperture_current = aperture['Current']
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'camera.html',
|
||||||
|
shutterspeed_choices=shutterspeed_choices,
|
||||||
|
shutterspeed_current=shutterspeed_current,
|
||||||
|
aperture_choices=aperture_choices,
|
||||||
|
aperture_current=aperture_current
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/set', methods=['POST'])
|
||||||
|
def set_camera_settings():
|
||||||
|
"""
|
||||||
|
Receives and processes new camera settings (shutterspeed, aperture) from the client.
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
shutterspeed = data.get('shutterspeed')
|
||||||
|
aperture = data.get('aperture')
|
||||||
|
|
||||||
|
if shutterspeed is not None:
|
||||||
|
print(f"Received shutterspeed: {shutterspeed}")
|
||||||
|
C.set_config('shutterspeed', shutterspeed)
|
||||||
|
|
||||||
|
if aperture is not None:
|
||||||
|
print(f"Received aperture: {aperture}")
|
||||||
|
C.set_config('aperture', aperture)
|
||||||
|
|
||||||
|
return {'status': 'ok', 'shutterspeed': shutterspeed, 'aperture': aperture}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# filepath: /home/nicolas/dev/git/git.polymny.net/source/nenuscanner/src/nenuscanner/routes/camera.py
|
||||||
|
from flask import send_file
|
||||||
|
|
||||||
|
@blueprint.route('/feed.jpg')
|
||||||
|
def camera_feed():
|
||||||
|
return send_file('static/feed.jpg', mimetype='image/jpeg')
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">Configurer la camera </h1>
|
||||||
|
|
||||||
|
<div class="field is-grouped is-grouped-multiline">
|
||||||
|
<div class="control">
|
||||||
|
<button> Lire la configuration actuelle</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="/camera/set" method="POST">
|
||||||
|
<div class="columns">
|
||||||
|
<!-- Left column: Camera controls -->
|
||||||
|
<div class="column is-half">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="shutterspeed">Shutter Speed:</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="shutterspeed" name="shutterspeed">
|
||||||
|
{% for choice in shutterspeed_choices %}
|
||||||
|
<option value="{{ choice.value }}" {% if choice.label == shutterspeed_current %}selected{% endif %}>
|
||||||
|
{{ choice.value }} {{ choice.label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control mt-2">
|
||||||
|
<button class="button is-small is-primary" name="validate-shutterspeed">Set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="aperture">Aperture:</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="aperture" name="aperture">
|
||||||
|
{% for choice in aperture_choices %}
|
||||||
|
<option value="{{ choice.value }}" {% if choice.label == aperture_current %}selected{% endif %}>
|
||||||
|
{{ choice.value }} {{ choice.label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control mt-2">
|
||||||
|
<button class="button is-small is-primary" name="validate-aperture" type="button">Set</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Right column: Camera preview -->
|
||||||
|
<div class="column is-half has-text-centered">
|
||||||
|
<figure class="image is-4by3" style="max-width: 480px; margin: auto;">
|
||||||
|
<img id="camera-preview" src="/camera/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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block extrajs %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.querySelector('button[name="validate-shutterspeed"]').addEventListener('click', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const value = document.getElementById('shutterspeed').value;
|
||||||
|
await fetch('/camera/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ shutterspeed: value })
|
||||||
|
});
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('button[name="validate-aperture"]').addEventListener('click', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const value = document.getElementById('aperture').value;
|
||||||
|
await fetch('/camera/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ aperture: value })
|
||||||
|
});
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh preview image without reloading the page
|
||||||
|
window.refreshPreview = function() {
|
||||||
|
const img = document.getElementById('camera-preview');
|
||||||
|
const url = '/camera/feed.jpg?' + new Date().getTime();
|
||||||
|
img.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically refresh every 2 seconds
|
||||||
|
setInterval(window.refreshPreview, 2000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock extrajs %}
|
||||||
Loading…
Reference in New Issue