Compare commits
67 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
90bb0e69c0 | |
|
|
0cbc102508 | |
|
|
7c5ab98553 | |
|
|
5fba1ce900 | |
|
|
35b039a39a | |
|
|
80d259e519 | |
|
|
84a51af78d | |
|
|
13c9bdfd25 | |
|
|
e41f0b92d8 | |
|
|
d75427eac1 | |
|
|
9d40f213be | |
|
|
7d9360576f | |
|
|
7ea52bc571 | |
|
|
7eb06bbd57 | |
|
|
5c3bb99d52 | |
|
|
8d1dff3470 | |
|
|
90866d01f8 | |
|
|
80ba6b40e8 | |
|
|
ef8d988736 | |
|
|
cbee1cb4b2 | |
|
|
76b13da56c | |
|
|
61c64d47fd | |
|
|
2a33ad0b49 | |
|
|
f3e34ddc86 | |
|
|
732e24fd63 | |
|
|
ec1798e150 | |
|
|
48c3e66595 | |
|
|
6d955dd09a | |
|
|
bfe4a07be8 | |
|
|
9eb3c6af0b | |
|
|
01f59f55f2 | |
|
|
9a64bc47cb | |
|
|
ace1b4fd9a | |
|
|
745f95af8c | |
|
|
00789c63bd | |
|
|
727ab37b0b | |
|
|
62b6bb2417 | |
|
|
8d13713c5f | |
|
|
2e4238fc60 | |
|
|
3a63449c83 | |
|
|
0acb2f0a0c | |
|
|
6b4cb074eb | |
|
|
c02991eb44 | |
|
|
8cfcf1ec55 | |
|
|
a658be3216 | |
|
|
7e3787053a | |
|
|
a01c73d228 | |
|
|
e769611395 | |
|
|
9be861b01c | |
|
|
d135d50f94 | |
|
|
809d11cb49 | |
|
|
68361ac92b | |
|
|
f6f777662e | |
|
|
77f0c2efba | |
|
|
eddd0853b5 | |
|
|
be9d8128d6 | |
|
|
84b5703f64 | |
|
|
4889aeaf7c | |
|
|
d3f1253376 | |
|
|
38a92d4a73 | |
|
|
e1c4d5179e | |
|
|
70383ebb34 | |
|
|
6671f2ad15 | |
|
|
9913c394f3 | |
|
|
b7d53c381c | |
|
|
1c7f7e648a | |
|
|
45401e3255 |
|
|
@ -1,9 +1,12 @@
|
|||
# Config file
|
||||
config.py
|
||||
|
||||
.device
|
||||
data
|
||||
data-*
|
||||
secret.py
|
||||
src/nenuscanner/secret.py
|
||||
src/nenuscanner/static/calibration-visualiser.*
|
||||
node_modules
|
||||
static/calibration-visualiser.*
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||
|
|
@ -181,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
|
||||
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
all: ts/*ts
|
||||
if [ ! -f ./node_modules/typescript/bin/tsc ]; then npm install; fi
|
||||
tsc
|
||||
esbuild ts/main.ts --bundle --minify --sourcemap --target=firefox57 --outfile=static/calibration-visualiser.js
|
||||
npm install
|
||||
./node_modules/typescript/bin/tsc
|
||||
./node_modules/esbuild/bin/esbuild ts/main.ts --bundle --minify --sourcemap --target=firefox58 --outfile=src/nenuscanner/static/calibration-visualiser.js
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
# 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 18 | LED 3 |
|
||||
| GPIO 27 | LED 5 |
|
||||
| GPIO 23 | LED 6 |
|
||||
| GPIO 23 | LED 6 |
|
||||
|
||||
|
||||
|
||||
## Appareil photo
|
||||
|
||||
|
||||
### Réglages EOS 7D
|
||||
|
||||
Prise de photo:
|
||||
```
|
||||
gphoto2 --capture-image-and-download
|
||||
```
|
||||
|
||||
#### Format image
|
||||
|
||||
Télécharger RAW et JPEG
|
||||
|
||||
```
|
||||
gphoto2 --set-config imageformat=10
|
||||
#Choice: 10 RAW + Small Fine JPEG
|
||||
```
|
||||
|
||||
|
||||
|
||||
Liste des formats:
|
||||
```
|
||||
gphoto2 --get-config imageformat
|
||||
```
|
||||
|
||||
#### Capture target
|
||||
|
||||
Stock sur la mémoire interne ou SD de l'apparail
|
||||
|
||||
Sur la mémoire innterne, au download c'est toujours le mêm fichier (écrasement)
|
||||
|
||||
```
|
||||
gphoto2 --get-config capturetarget 0 ↵
|
||||
|
||||
Label: Capture Target
|
||||
Readonly: 0
|
||||
Type: RADIO
|
||||
Current: Memory card
|
||||
Choice: 0 Internal RAM
|
||||
Choice: 1 Memory card
|
||||
```
|
||||
En prod choix 0 : on ne conserve pas les photos sur l'appareil
|
||||
Et test choix 1 : on conserve sur l'appareil
|
||||
|
||||
Exemple choix 1:
|
||||
```
|
||||
gphoto2 --set-config capturetarget=1
|
||||
```
|
||||
|
||||
|
||||
# ISO
|
||||
|
||||
ISO 100
|
||||
|
||||
```
|
||||
gphoto2 --set-config iso=1
|
||||
```
|
||||
|
||||
### réglages objectif
|
||||
|
||||
| Ouverture | aperture | gphoto2 --set-config aperture=14 | Ouverture f/10 |
|
||||
| temps de pose | shutterspeed | gphoto2 --set-config shutterspeed=35 | Vitesse 1/100 s |
|
||||
| Picture Style | picturestyle | gphoto2 --set-config picturestyle=0 | Standard |
|
||||
| Exposition | aeb | gphoto2 --set-config aeb=0 | 0 |
|
||||
|
||||
|
||||
## 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
|
||||
TE connectivity 2-2154857-2
|
||||
|
||||
Arrow: https://www.arrow.com/en/products/2-2154857-2/te-connectivity
|
||||
|
||||
### Dissipateur
|
||||
|
||||
MeachaTronix LED heatsink series LPF_ZHC and LPF_ZHP
|
||||
|
||||
LPF6768-ZHP-B
|
||||
|
||||
https://www.elpro.org/gb/mechatronix-lpfzhc-and-lpfzhp-series/106105-lpf6768-zhp-b.html
|
||||
327
app.py
327
app.py
|
|
@ -1,327 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from flask import Flask, redirect, request, render_template, send_from_directory, session
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
from os.path import join
|
||||
import sqlite3
|
||||
import uuid
|
||||
from . import db, config, scanner, calibration, archive
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Manage secret key
|
||||
try:
|
||||
from . import secret
|
||||
app.config['SECRET_KEY'] = secret.SECRET_KEY
|
||||
except ImportError:
|
||||
# Secret key file does not exist, create it
|
||||
secret = os.urandom(50).hex()
|
||||
with open('secret.py', 'w') as f:
|
||||
f.write(f'SECRET_KEY = "{secret}"')
|
||||
app.config['SECRET_KEY'] = secret
|
||||
|
||||
|
||||
def get_calibration(conn: sqlite3.Connection) -> db.Calibration:
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
if calibration_id is None:
|
||||
return db.Calibration.Dummy
|
||||
|
||||
return db.Calibration.get_from_id(calibration_id, conn)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_stage_and_region():
|
||||
conn = db.get()
|
||||
return dict(calibration=get_calibration(conn), leds=config.LEDS_UUIDS, CalibrationState=db.CalibrationState)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def manage_auto_use_last_calibration():
|
||||
if config.AUTO_USE_LAST_CALIBRATION and 'calibration_id' not in session:
|
||||
conn = db.get()
|
||||
last = db.Calibration.get_last(conn)
|
||||
if last is not None:
|
||||
session['calibration_id'] = last.id
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
conn = db.get()
|
||||
objects = db.Object.all(conn)
|
||||
return render_template('index.html', objects=objects)
|
||||
|
||||
|
||||
@app.route("/create-object/", methods=["POST"])
|
||||
def create_object():
|
||||
conn = db.get()
|
||||
with conn:
|
||||
db.Object.create(request.form.get('name'), conn)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@app.route('/object/<id>')
|
||||
def object(id: int):
|
||||
conn = db.get()
|
||||
object = db.Object.get_from_id(id, conn).full(conn)
|
||||
return render_template('object.html', object=object)
|
||||
|
||||
|
||||
@app.route('/scan/<id>')
|
||||
def scan(id: int):
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
object = db.Object.get_from_id(id, conn)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
return render_template('scan.html', object=object)
|
||||
|
||||
|
||||
@app.route('/scan-acquisition/<id>')
|
||||
def scan_existing(id: int):
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
acquisition = db.Acquisition.get_from_id(id, conn)
|
||||
object = acquisition.object(conn)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
return render_template('scan.html', object=object, acquisition=acquisition)
|
||||
|
||||
|
||||
@app.route("/calibrate/")
|
||||
def calibrate():
|
||||
conn = db.get()
|
||||
if 'calibration_id' not in session:
|
||||
with conn:
|
||||
calibration = db.Calibration.create(conn)
|
||||
session['calibration_id'] = calibration.id
|
||||
else:
|
||||
calibration = db.Calibration.get_from_id(session['calibration_id'], conn)
|
||||
|
||||
if calibration.state in [db.CalibrationState.Empty, db.CalibrationState.HasData]:
|
||||
return render_template('calibrate.html')
|
||||
else:
|
||||
return render_template('calibration.html', calibration=calibration)
|
||||
|
||||
|
||||
@app.route("/new-calibration")
|
||||
def new_calibration():
|
||||
conn = db.get()
|
||||
with conn:
|
||||
calibration = db.Calibration.create(conn)
|
||||
session['calibration_id'] = calibration.id
|
||||
|
||||
return redirect('/calibrate')
|
||||
|
||||
|
||||
@app.route("/cancel-calibration")
|
||||
def cancel_calibration():
|
||||
conn = db.get()
|
||||
calibration = db.Calibration.get_from_id(session['calibration_id'], conn)
|
||||
calibration.state = db.CalibrationState.HasData
|
||||
with conn:
|
||||
calibration.save(conn)
|
||||
return redirect('/calibrate')
|
||||
|
||||
|
||||
@app.route("/api/preview/<id>")
|
||||
def preview(id: int):
|
||||
conn = db.get()
|
||||
db.Object.get_from_id(id, conn)
|
||||
capture_uuid = uuid.uuid4()
|
||||
if scanner.capture(join(config.DATA_DIR, str(id), 'previews', str(capture_uuid) + '.jpg')):
|
||||
return str(capture_uuid)
|
||||
else:
|
||||
return "Impossible de capturer l'image.", 500
|
||||
|
||||
|
||||
@app.route("/api/scan-for-calibration")
|
||||
def scan_calibration():
|
||||
conn = db.get()
|
||||
|
||||
if 'calibration_id' not in session:
|
||||
with conn:
|
||||
calibration = db.Calibration.create(conn)
|
||||
calibration_id = str(calibration.id)
|
||||
session['calibration_id'] = calibration.id
|
||||
else:
|
||||
calibration_id = str(session['calibration_id'])
|
||||
calibration = get_calibration(conn)
|
||||
|
||||
def generate():
|
||||
length = len(config.LEDS_UUIDS)
|
||||
for index, led_uuid in enumerate(scanner.scan(join(config.CALIBRATION_DIR, calibration_id))):
|
||||
yield f"{led_uuid},{(index+1)/length}\n"
|
||||
|
||||
with conn:
|
||||
calibration.state = db.CalibrationState.HasData
|
||||
calibration.save(conn)
|
||||
|
||||
return app.response_class(generate(), mimetype='text/plain')
|
||||
|
||||
|
||||
@app.route("/api/scan-for-object/<object_id>")
|
||||
def scan_object(object_id: int):
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
object = db.Object.get_from_id(object_id, conn)
|
||||
|
||||
if object is None:
|
||||
raise RuntimeError(f"Aucun objet d'id {object_id}")
|
||||
|
||||
with conn:
|
||||
acquisition = object.add_acquisition(calibration_id, conn)
|
||||
|
||||
def generate():
|
||||
yield str(acquisition.id)
|
||||
length = len(config.LEDS_UUIDS)
|
||||
for index, led_uuid in enumerate(scanner.scan(join(config.OBJECT_DIR, str(object.id), str(acquisition.id)))):
|
||||
yield f"{led_uuid},{(index+1)/length}\n"
|
||||
|
||||
return app.response_class(generate(), mimetype='text/plain')
|
||||
|
||||
|
||||
@app.route("/api/scan-for-acquisition/<acquisition_id>")
|
||||
def scan_acquisition(acquisition_id: int):
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
acquisition = db.Acquisition.get_from_id(acquisition_id, conn)
|
||||
|
||||
if acquisition is None:
|
||||
raise RuntimeError(f"Aucun acquisition d'id {acquisition_id}")
|
||||
|
||||
object = acquisition.object(conn)
|
||||
|
||||
def generate():
|
||||
length = len(config.LEDS_UUIDS)
|
||||
for index, led_uuid in enumerate(scanner.scan(join(config.OBJECT_DIR, str(object.id), str(acquisition.id)))):
|
||||
yield f"{led_uuid},{(index+1)/length}\n"
|
||||
|
||||
return app.response_class(generate(), mimetype='text/plain')
|
||||
|
||||
|
||||
@app.route("/validate-acquisition/<acquisition_id>")
|
||||
def validate_acquisition(acquisition_id: int):
|
||||
conn = db.get()
|
||||
acquisition = db.Acquisition.get_from_id(acquisition_id, conn)
|
||||
|
||||
if acquisition is None:
|
||||
raise f"Aucune acquisition d'id {acquisition_id}"
|
||||
|
||||
object = acquisition.object(conn)
|
||||
|
||||
acquisition.validated = True
|
||||
with conn:
|
||||
acquisition.save(conn)
|
||||
|
||||
return redirect(f'/object/{object.id}')
|
||||
|
||||
|
||||
@app.route('/api/use-last-calibration')
|
||||
def use_last_calibration():
|
||||
conn = db.get()
|
||||
calibration = db.Calibration.get_last(conn)
|
||||
session['calibration_id'] = calibration.id
|
||||
return 'ok'
|
||||
|
||||
|
||||
@app.route("/api/calibrate")
|
||||
def run_calibration():
|
||||
conn = db.get()
|
||||
id = session['calibration_id']
|
||||
calib = db.Calibration.get_from_id(id, conn)
|
||||
if calib is None:
|
||||
return 'oops', 404
|
||||
|
||||
calibration_json = calibration.calibrate(join(config.CALIBRATION_DIR, str(id)))
|
||||
with open(join(config.CALIBRATION_DIR, str(id), 'calibration.json'), 'w') as f:
|
||||
json.dump(calibration_json, f, indent=4)
|
||||
with conn:
|
||||
calib.state = db.CalibrationState.IsComputed
|
||||
calib.save(conn)
|
||||
|
||||
return 'ok'
|
||||
|
||||
|
||||
@app.route('/validate-calibration')
|
||||
def validate_calibration():
|
||||
conn = db.get()
|
||||
calib = get_calibration(conn)
|
||||
if calib is None:
|
||||
return 'oops', 404
|
||||
|
||||
with conn:
|
||||
calib.validate(conn)
|
||||
|
||||
return redirect('/')
|
||||
|
||||
|
||||
def download_object(id: int, tar: archive.ArchiveSender):
|
||||
conn = db.get()
|
||||
object = db.Object.get_from_id(id, conn).full(conn)
|
||||
|
||||
# Group acquisitions sharing calibration
|
||||
def keyfunc(x: db.Calibration) -> int:
|
||||
return x.calibration_id
|
||||
|
||||
acquisitions_sorted = sorted(object.acquisitions, key=keyfunc)
|
||||
acquisitions_grouped = [
|
||||
(db.Calibration.get_from_id(k, conn), list(g))
|
||||
for k, g in itertools.groupby(acquisitions_sorted, key=keyfunc)
|
||||
]
|
||||
|
||||
# Create archive file to send
|
||||
for calibration_index, (calib, acquisitions) in enumerate(acquisitions_grouped):
|
||||
calibration_dir = join(config.CALIBRATION_DIR, str(calib.id))
|
||||
|
||||
# Add calibration images
|
||||
for image in os.listdir(calibration_dir):
|
||||
tar.add_file(
|
||||
f'object/{calibration_index}/calibration/{image}',
|
||||
join(calibration_dir, image)
|
||||
)
|
||||
|
||||
# Add each acquisition
|
||||
for acquisition_index, acquisition in enumerate(acquisitions):
|
||||
acquisition_dir = join(config.OBJECT_DIR, str(object.id), str(acquisition.id))
|
||||
|
||||
for image in os.listdir(acquisition_dir):
|
||||
tar.add_file(
|
||||
f'object/{calibration_index}/{acquisition_index}/{image}',
|
||||
join(acquisition_dir, image)
|
||||
)
|
||||
|
||||
return tar.response()
|
||||
|
||||
|
||||
@app.route('/download-object/tar/<id>')
|
||||
def download_object_tar(id: int):
|
||||
return download_object(id, archive.TarSender())
|
||||
|
||||
|
||||
@app.route('/download-object/zip/<id>')
|
||||
def download_object_zip(id: int):
|
||||
return download_object(id, archive.ZipSender())
|
||||
|
||||
|
||||
@app.route('/static/<path:path>')
|
||||
def send_static(path):
|
||||
return send_from_directory('static', path)
|
||||
|
||||
|
||||
@app.route('/data/<path:path>')
|
||||
def send_data(path):
|
||||
return send_from_directory('data', path)
|
||||
|
|
@ -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())
|
||||
24
config.py
24
config.py
|
|
@ -1,24 +0,0 @@
|
|||
from os.path import join
|
||||
|
||||
DATA_DIR = 'data'
|
||||
BACKUPS_DIR = 'data-backups'
|
||||
CALIBRATION_DIR = join(DATA_DIR, 'calibrations')
|
||||
OBJECT_DIR = join(DATA_DIR, 'objects')
|
||||
DATABASE_PATH = join(DATA_DIR, 'db.sqlite')
|
||||
|
||||
AUTO_USE_LAST_CALIBRATION = False
|
||||
|
||||
LEDS_UUIDS = [
|
||||
'ac59350e-3787-46d2-88fa-743c1d34fe86',
|
||||
'83ab3133-d4de-4a42-99a7-8f8a3f3732ba',
|
||||
'577d6e72-f518-4d65-af28-f8faf1ca3f5d',
|
||||
'ec49a20c-bddd-4614-b828-fa45e01bfb19',
|
||||
'5c249cce-d2b6-4b56-96c8-5caa43d8f040',
|
||||
'22d783fb-ae55-4581-a3c6-e010d9d5e9de',
|
||||
'12cb6a32-04a6-433b-8146-b753b8f1286d',
|
||||
'461255a3-259a-4291-adc3-2fb736231a04',
|
||||
'3896662f-9826-4445-ad70-86c2e6c143e7',
|
||||
'f87698ec-3cba-42fe-9a61-5ac9f77d767a',
|
||||
'4c77a655-4b68-4557-a696-29345c6676a1',
|
||||
'b1cfe287-aa3b-445e-bcdc-75ae375efe43',
|
||||
]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=NenuScanner
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
ExecStart=nenuscanner
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
|
@ -10,7 +10,393 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/three": "^0.165.0",
|
||||
"three": "^0.165.0"
|
||||
"esbuild": "^0.24.0",
|
||||
"three": "^0.165.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
|
||||
"integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
|
||||
"integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
|
||||
"integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
|
||||
"integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
|
||||
"integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
|
||||
"integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
|
||||
"integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
|
||||
"integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
|
||||
"integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
|
||||
"integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
|
||||
"integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
|
||||
"integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
|
|
@ -44,6 +430,45 @@
|
|||
"integrity": "sha512-JYcclaQIlisHRXM9dMF7SeVvQ54kcYc7QK1eKCExCTLKWnZDxP4cp/rXH4Uoa1j5+5oQJ0Cc2sZC/PWiiG4q2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
|
||||
"integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.24.0",
|
||||
"@esbuild/android-arm": "0.24.0",
|
||||
"@esbuild/android-arm64": "0.24.0",
|
||||
"@esbuild/android-x64": "0.24.0",
|
||||
"@esbuild/darwin-arm64": "0.24.0",
|
||||
"@esbuild/darwin-x64": "0.24.0",
|
||||
"@esbuild/freebsd-arm64": "0.24.0",
|
||||
"@esbuild/freebsd-x64": "0.24.0",
|
||||
"@esbuild/linux-arm": "0.24.0",
|
||||
"@esbuild/linux-arm64": "0.24.0",
|
||||
"@esbuild/linux-ia32": "0.24.0",
|
||||
"@esbuild/linux-loong64": "0.24.0",
|
||||
"@esbuild/linux-mips64el": "0.24.0",
|
||||
"@esbuild/linux-ppc64": "0.24.0",
|
||||
"@esbuild/linux-riscv64": "0.24.0",
|
||||
"@esbuild/linux-s390x": "0.24.0",
|
||||
"@esbuild/linux-x64": "0.24.0",
|
||||
"@esbuild/netbsd-x64": "0.24.0",
|
||||
"@esbuild/openbsd-arm64": "0.24.0",
|
||||
"@esbuild/openbsd-x64": "0.24.0",
|
||||
"@esbuild/sunos-x64": "0.24.0",
|
||||
"@esbuild/win32-arm64": "0.24.0",
|
||||
"@esbuild/win32-ia32": "0.24.0",
|
||||
"@esbuild/win32-x64": "0.24.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
|
|
@ -61,6 +486,19 @@
|
|||
"resolved": "https://registry.npmjs.org/three/-/three-0.165.0.tgz",
|
||||
"integrity": "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
"description": "",
|
||||
"dependencies": {
|
||||
"@types/three": "^0.165.0",
|
||||
"three": "^0.165.0"
|
||||
"esbuild": "^0.24.0",
|
||||
"three": "^0.165.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
[project]
|
||||
name = "nenuscanner"
|
||||
version = "0.1.0"
|
||||
dependencies = ["flask"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
nenuscanner = ["templates/*.html", "static/*"]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Flask
|
||||
pillow
|
||||
gphoto2
|
||||
scipy
|
||||
waitress
|
||||
47
scanner.py
47
scanner.py
|
|
@ -1,47 +0,0 @@
|
|||
import cv2
|
||||
import os
|
||||
from os.path import join
|
||||
import shutil
|
||||
import time
|
||||
from . import config
|
||||
|
||||
# Delay between to captures
|
||||
DELAY = 0.5
|
||||
|
||||
|
||||
def capture(output_path: str) -> bool:
|
||||
try:
|
||||
with open('.device', 'r') as f:
|
||||
device_id = int(f.read().rstrip())
|
||||
except:
|
||||
device_id = 0
|
||||
|
||||
cam = cv2.VideoCapture(device_id)
|
||||
s, img = cam.read()
|
||||
if s:
|
||||
cv2.imwrite(output_path, img)
|
||||
cam.release()
|
||||
return s
|
||||
|
||||
|
||||
def scan(output_dir: str):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
for led in config.LEDS_UUIDS:
|
||||
print(f'Turn on {led}')
|
||||
img = join(output_dir, led + '.jpg')
|
||||
|
||||
# Measure the time it takes to capture
|
||||
start = time.time()
|
||||
|
||||
# capture(img)
|
||||
# For debug purposes
|
||||
shutil.copyfile(join('data-keep/small', led + '.jpg'), img)
|
||||
|
||||
delta = time.time() - start
|
||||
|
||||
# Wait for at least one second between each capture
|
||||
if delta < DELAY:
|
||||
time.sleep(DELAY - delta)
|
||||
|
||||
print(f'Turn off {led}')
|
||||
yield led
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
from flask import Flask, send_from_directory, session
|
||||
import os
|
||||
from . import db, config, routes, utils, leds
|
||||
|
||||
leds.get().enter()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Manage secret key
|
||||
try:
|
||||
from . import secret
|
||||
app.config['SECRET_KEY'] = secret.SECRET_KEY
|
||||
except ImportError:
|
||||
# Secret key file does not exist, create it
|
||||
secret = os.urandom(50).hex()
|
||||
with open('secret.py', 'w') as f:
|
||||
f.write(f'SECRET_KEY = "{secret}"')
|
||||
app.config['SECRET_KEY'] = secret
|
||||
|
||||
|
||||
# Middlewares to help us deal with stuff
|
||||
@app.context_processor
|
||||
def inject():
|
||||
"""
|
||||
Returns a dictionnary with the uuids of leds and the calibration state.
|
||||
"""
|
||||
conn = db.get()
|
||||
return {
|
||||
'calibration': utils.get_calibration(conn),
|
||||
'leds': leds.get().leds,
|
||||
'CalibrationState': db.CalibrationState,
|
||||
}
|
||||
|
||||
|
||||
@app.before_request
|
||||
def manage_auto_use_last_calibration():
|
||||
"""
|
||||
Automatically use the last calibration if the config is set accordingly.
|
||||
"""
|
||||
if config.AUTO_USE_LAST_CALIBRATION and 'calibration_id' not in session:
|
||||
conn = db.get()
|
||||
last = db.Calibration.get_last(conn)
|
||||
if last is not None:
|
||||
session['calibration_id'] = last.id
|
||||
|
||||
|
||||
app.register_blueprint(routes.blueprint)
|
||||
|
||||
|
||||
@app.route('/static/<path:path>')
|
||||
def send_static(path):
|
||||
return send_from_directory('static', path)
|
||||
|
||||
|
||||
@app.route('/data/<path:path>')
|
||||
def send_data(path):
|
||||
return send_from_directory(config.DATA_DIR, path)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
from . import app
|
||||
from waitress import serve
|
||||
|
||||
|
||||
def main():
|
||||
port = os.environ.get('FLASK_RUN_PORT', 8000)
|
||||
print(f'Starting server on port {port}')
|
||||
serve(app, listen=f'*:{port}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -4,6 +4,8 @@ from flask import Response
|
|||
import functools
|
||||
import os
|
||||
import zlib
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
# Chunks for crc 32 computation
|
||||
CRC32_CHUNK_SIZE = 65_536
|
||||
|
|
@ -19,6 +21,13 @@ ZERO = ord('0')
|
|||
|
||||
|
||||
def tar_header_chunk(filename: str, filepath: str) -> bytes:
|
||||
"""
|
||||
Returns the 512 bytes header for a tar file in a tar archive.
|
||||
|
||||
Args:
|
||||
filename (str): path of where the file will be in the archive.
|
||||
filepath (str): path of where the file is currently on the disk.
|
||||
"""
|
||||
|
||||
# Returns the octal representation without the initial
|
||||
def oct(i: int) -> str:
|
||||
|
|
@ -65,17 +74,76 @@ def tar_header_chunk(filename: str, filepath: str) -> bytes:
|
|||
|
||||
|
||||
class ArchiveSender:
|
||||
"""
|
||||
Helper class to send archives over the network.
|
||||
|
||||
This class is abstract, and needs to be derived by specific archive sender classes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Creates a new archive sender.
|
||||
"""
|
||||
self.files: dict[str, str] = {}
|
||||
|
||||
def add_file(self, filename: str, filepath: str):
|
||||
"""
|
||||
Adds a file to the archive.
|
||||
|
||||
Args:
|
||||
filename (str): path of where the file will be in the archive.
|
||||
filepath (str): path of where the file is currently on the disk.
|
||||
"""
|
||||
self.files[filename] = filepath
|
||||
|
||||
def response(self):
|
||||
def content_length(self) -> Optional[int]:
|
||||
"""
|
||||
Returns the size of the archive if it is computable beforehand, none otherwise.
|
||||
"""
|
||||
return None
|
||||
|
||||
def generator(self):
|
||||
"""
|
||||
Returns a generator that yields the bytes of the archive.
|
||||
"""
|
||||
raise NotImplementedError("Abstract method")
|
||||
|
||||
def mime_type(self) -> str:
|
||||
"""
|
||||
Returns the mime type of the archive.
|
||||
"""
|
||||
raise NotImplementedError("Abstract method")
|
||||
|
||||
def archive_name(self) -> str:
|
||||
"""
|
||||
Returns the name of the archive.
|
||||
|
||||
This method is useful for web applications where the archive will be downloaded.
|
||||
"""
|
||||
raise NotImplementedError("Abstract method")
|
||||
|
||||
def response(self) -> Response:
|
||||
"""
|
||||
Returns a flask reponse for the archive.
|
||||
"""
|
||||
headers = {'Content-Disposition': f'attachment; filename="{self.archive_name()}"'}
|
||||
|
||||
length = self.content_length()
|
||||
if length is not None:
|
||||
headers['Content-Length'] = str(length)
|
||||
|
||||
return Response(
|
||||
self.generator(),
|
||||
mimetype=self.mime_type(),
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
class TarSender(ArchiveSender):
|
||||
"""
|
||||
A sender for tar archives computed on the fly.
|
||||
"""
|
||||
|
||||
def generator(self):
|
||||
def generate():
|
||||
for name, file in self.files.items():
|
||||
|
|
@ -98,15 +166,31 @@ class TarSender(ArchiveSender):
|
|||
yield b'\x00' * (512 - bytes_sent % 512)
|
||||
return generate()
|
||||
|
||||
def response(self):
|
||||
return Response(
|
||||
self.generator(),
|
||||
mimetype='application/x-tar',
|
||||
headers={'Content-Disposition': 'attachment; filename="archive.tar"'}
|
||||
)
|
||||
def mime_type(self) -> str:
|
||||
return 'application/x-tar'
|
||||
|
||||
def archive_name(self) -> str:
|
||||
return 'archive.tar'
|
||||
|
||||
def content_length(self) -> int:
|
||||
length = 0
|
||||
|
||||
for file in self.files.values():
|
||||
stat = os.stat(file)
|
||||
|
||||
# Add size of header, and size of content ceiled to 512 bytes
|
||||
length += 512 + stat.st_size + (512 - stat.st_size % 512)
|
||||
|
||||
return length
|
||||
|
||||
|
||||
def crc32(filename) -> int:
|
||||
"""
|
||||
Computes the CRC32 checksum for the file.
|
||||
|
||||
Args:
|
||||
filename (str): path to the file of which the CRC32 needs to be computed.
|
||||
"""
|
||||
with open(filename, 'rb') as fh:
|
||||
hash = 0
|
||||
while True:
|
||||
|
|
@ -118,6 +202,17 @@ def crc32(filename) -> int:
|
|||
|
||||
|
||||
def zip_local_file_header(filename: str, filepath: str, crc: int) -> bytes:
|
||||
"""
|
||||
Generates the bytes for the local file header of the file.
|
||||
|
||||
Args:
|
||||
filename (str): path of where the file will be in the archive.
|
||||
filepath (str): path of where the file is currently on the disk.
|
||||
crc (int):
|
||||
the CRC 32 checksum of the file. It is not computed by this function because it is also required in the
|
||||
central directory file header, so the user of this function should compute it beforehand, and reuse it later
|
||||
to avoid computing it twice.
|
||||
"""
|
||||
buffer_size = 30 + len(filename)
|
||||
buffer = bytearray(buffer_size)
|
||||
stat = os.stat(filepath)
|
||||
|
|
@ -158,6 +253,18 @@ def zip_local_file_header(filename: str, filepath: str, crc: int) -> bytes:
|
|||
|
||||
|
||||
def zip_central_directory_file_header(filename: str, filepath: str, crc: int, offset: int) -> bytes:
|
||||
"""
|
||||
Generates the bytes for the central directory file header of the file.
|
||||
|
||||
Args:
|
||||
filename (str): path of where the file will be in the archive.
|
||||
filepath (str): path of where the file is currently on the disk.
|
||||
crc (int):
|
||||
the CRC 32 checksum of the file. It is not computed by this function because it is also required in the
|
||||
local file header, so the user of this function should compute it beforehand, and reuse it later to avoid
|
||||
computing it twice.
|
||||
offset (int): number of bytes where the file starts.
|
||||
"""
|
||||
buffer_size = 46 + len(filename)
|
||||
buffer = bytearray(buffer_size)
|
||||
stat = os.stat(filepath)
|
||||
|
|
@ -212,6 +319,14 @@ def zip_central_directory_file_header(filename: str, filepath: str, crc: int, of
|
|||
|
||||
|
||||
def zip_end_of_central_directory(items_number: int, central_directory_size: int, central_directory_offset: int):
|
||||
"""
|
||||
Generates the bytes for the end of central directory of the archive.
|
||||
|
||||
Args:
|
||||
items_number (int): number of files in the archive.
|
||||
central_directory_size (int): size in bytes of the central directory.
|
||||
central_directory_offset (int): number of the byte where the central directory starts.
|
||||
"""
|
||||
buffer = bytearray(22)
|
||||
# Field 1: End of central directory signature = 0x06054b50 (buffer[0:4])
|
||||
buffer[0:4] = b'\x50\x4b\x05\x06'
|
||||
|
|
@ -239,6 +354,10 @@ def zip_end_of_central_directory(items_number: int, central_directory_size: int,
|
|||
|
||||
|
||||
class ZipSender(ArchiveSender):
|
||||
"""
|
||||
A sender for zip archives computed on the fly.
|
||||
"""
|
||||
|
||||
def generator(self):
|
||||
def generate():
|
||||
local_offsets = dict()
|
||||
|
|
@ -277,9 +396,20 @@ class ZipSender(ArchiveSender):
|
|||
|
||||
return generate()
|
||||
|
||||
def response(self):
|
||||
return Response(
|
||||
self.generator(),
|
||||
mimetype='application/zip',
|
||||
headers={'Content-Disposition': 'attachment; filename="archive.zip"'}
|
||||
)
|
||||
def content_length(self) -> int:
|
||||
length = 0
|
||||
|
||||
for name, file in self.files.items():
|
||||
stat = os.stat(file)
|
||||
|
||||
# Add size of local file header, central directory file header and file size
|
||||
length += 76 + 2 * len(name) + stat.st_size
|
||||
|
||||
# Add size of end of central directory
|
||||
return length + 22
|
||||
|
||||
def mime_type(self) -> str:
|
||||
return 'application/zip'
|
||||
|
||||
def archive_name(self) -> str:
|
||||
return 'archive.zip'
|
||||
|
|
@ -7,7 +7,7 @@ import os
|
|||
import sys
|
||||
from PIL import Image
|
||||
|
||||
from . import utils
|
||||
from . import math_utils
|
||||
|
||||
|
||||
# To extract a few images and resize them at 20% of their size:
|
||||
|
|
@ -20,16 +20,19 @@ def print_error(msg: str):
|
|||
|
||||
def calibrate(input_dir: str):
|
||||
# Load all images
|
||||
image_names = list(filter(lambda x: x != 'calibration.json', sorted(os.listdir(input_dir))))
|
||||
image_names = sorted([
|
||||
x for x in os.listdir(input_dir)
|
||||
if x != 'calibration.json' and x != 'all_on.jpg' and x != 'all_off.jpg' and x.endswith('.jpg')
|
||||
])
|
||||
images = [np.asarray(Image.open(os.path.join(input_dir, x))) for x in image_names]
|
||||
|
||||
# Camera parameters
|
||||
nu, nv, nc = images[0].shape
|
||||
nspheres = 5
|
||||
nspheres = 4
|
||||
focal_mm = 35
|
||||
matrix_size = 24
|
||||
focal_pix = nu * focal_mm / matrix_size
|
||||
K = utils.build_K_matrix(focal_pix, nu/2, nv/2)
|
||||
K = math_utils.build_K_matrix(focal_pix, nu/2, nv/2)
|
||||
|
||||
# Max image: image of brightest pixels, helps spheres segmentation
|
||||
max_image = functools.reduce(np.maximum, images)
|
||||
|
|
@ -38,22 +41,23 @@ def calibrate(input_dir: str):
|
|||
pixels = np.reshape(max_image / 255.0, (-1, 3))
|
||||
|
||||
# Initialize parameters for GMM
|
||||
init_params = np.ones(2), np.broadcast_to(np.eye(3) * 0.1, (2, 3, 3)), np.asarray([[0, 0, 0], [1, 1, 1]])
|
||||
# init_params = np.ones(2), np.broadcast_to(np.eye(3) * 0.1, (2, 3, 3)), np.asarray([[0, 0, 0], [1, 1, 1]])
|
||||
|
||||
# Estimate GMM parameters and classify pixels
|
||||
estimated_params = utils.gaussian_mixture_estimation(pixels, init_params, it=10)
|
||||
classif = np.asarray(utils.maximum_likelihood(pixels, estimated_params), dtype=bool)
|
||||
# estimated_params = math_utils.gaussian_mixture_estimation(pixels, init_params, it=10)
|
||||
# classif = np.asarray(math_utils.maximum_likelihood(pixels, estimated_params), dtype=bool)
|
||||
|
||||
# Refine classification to select the appropriate binary mask
|
||||
rectified_classif = utils.select_binary_mask(classif, lambda mask: np.mean(pixels[mask]))
|
||||
# rectified_classif = math_utils.select_binary_mask(classif, lambda mask: np.mean(pixels[mask]))
|
||||
rectified_classif = np.mean(pixels, axis=-1) > 0.03
|
||||
|
||||
# Identify the largest connected components (spheres) and extract their borders
|
||||
sphere_masks = utils.get_greatest_components(np.reshape(rectified_classif, (nu, nv)), nspheres)
|
||||
border_masks = np.vectorize(utils.get_mask_border, signature='(u,v)->(u,v)')(sphere_masks)
|
||||
sphere_masks = math_utils.get_greatest_components(np.reshape(rectified_classif, (nu, nv)), nspheres)
|
||||
border_masks = np.vectorize(math_utils.get_mask_border, signature='(u,v)->(u,v)')(sphere_masks)
|
||||
|
||||
# Fit quadratic forms (ellipses) to the borders
|
||||
def fit_on_mask(border):
|
||||
return utils.fit_quadratic_form(utils.to_homogeneous(np.argwhere(border)))
|
||||
return math_utils.fit_quadratic_form(math_utils.to_homogeneous(np.argwhere(border)))
|
||||
|
||||
ellipse_quadratics = np.vectorize(fit_on_mask, signature='(u,v)->(t,t)')(border_masks)
|
||||
|
||||
|
|
@ -61,38 +65,38 @@ def calibrate(input_dir: str):
|
|||
calibrated_quadratics = np.swapaxes(K, -1, -2) @ ellipse_quadratics @ K
|
||||
|
||||
# Deproject the ellipse quadratics to sphere centers
|
||||
sphere_centers = utils.deproject_ellipse_to_sphere(calibrated_quadratics, 1)
|
||||
sphere_centers = math_utils.deproject_ellipse_to_sphere(calibrated_quadratics, 1)
|
||||
|
||||
# Create coordinates and calculate camera rays
|
||||
coordinates = np.stack(np.meshgrid(range(nu), range(nv), indexing='ij'), axis=-1)
|
||||
rays = utils.get_camera_rays(coordinates, K)
|
||||
rays = math_utils.get_camera_rays(coordinates, K)
|
||||
|
||||
# Find the intersections between the camera rays and the spheres
|
||||
sphere_points_map, sphere_geometric_masks = \
|
||||
utils.line_sphere_intersection(sphere_centers[:, np.newaxis, np.newaxis, :], 1, rays[np.newaxis, :, :, :])
|
||||
math_utils.line_sphere_intersection(sphere_centers[:, np.newaxis, np.newaxis, :], 1, rays[np.newaxis, :, :, :])
|
||||
|
||||
sphere_points = np.asarray([sphere_points_map[i, sphere_geometric_masks[i]] for i in range(nspheres)], dtype=object)
|
||||
sphere_normals = np.vectorize(utils.sphere_intersection_normal, signature='(v),()->()', otypes=[object])(sphere_centers, sphere_points)
|
||||
sphere_normals = np.vectorize(math_utils.sphere_intersection_normal, signature='(v),()->()', otypes=[object])(sphere_centers, sphere_points)
|
||||
|
||||
# Load grey values from images for the identified sphere regions
|
||||
def to_grayscale(image):
|
||||
return [np.mean(image, axis=-1)[sphere_geometric_masks[i]] / 255.0 for i in range(nspheres)]
|
||||
return [np.power(np.mean(image, axis=-1)[sphere_geometric_masks[i]] / 255.0, 1.0) for i in range(nspheres)]
|
||||
|
||||
grey_values = np.asarray(list(map(to_grayscale, images)), dtype=object)
|
||||
|
||||
# Estimate lighting conditions from sphere normals and grey values
|
||||
estimated_lights = np.vectorize(
|
||||
utils.estimate_light,
|
||||
math_utils.estimate_light,
|
||||
excluded=(2,),
|
||||
signature='(),()->(k)',
|
||||
otypes=[float]
|
||||
)(sphere_normals, grey_values, (0.1, 0.9))
|
||||
|
||||
# Calculate the positions of the light sources
|
||||
light_positions = utils.lines_intersections(sphere_centers, estimated_lights)
|
||||
light_positions = math_utils.lines_intersections(sphere_centers, estimated_lights)
|
||||
|
||||
# Calculate plane parameters from the sphere centers and intersect camera rays with the plane
|
||||
plane_normal, plane_alpha = utils.plane_parameters_from_points(sphere_centers)
|
||||
plane_normal, plane_alpha = math_utils.plane_parameters_from_points(sphere_centers)
|
||||
|
||||
# Return value as dictionnary
|
||||
return {
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import subprocess
|
||||
|
||||
from flask import jsonify
|
||||
import gphoto2 as gp
|
||||
import shutil
|
||||
from . import leds, config
|
||||
import subprocess
|
||||
import json
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
|
||||
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):
|
||||
self._entered = False
|
||||
self.inner = gp.Camera()
|
||||
|
||||
def __enter__(self):
|
||||
if not self._entered:
|
||||
self._entered = True
|
||||
self.inner.init()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
# self.inner.exit()
|
||||
pass
|
||||
|
||||
def capture(self):
|
||||
try:
|
||||
return self.inner.capture(gp.GP_CAPTURE_IMAGE)
|
||||
except Exception as e:
|
||||
print('An error occured when capturing photo', e)
|
||||
return None
|
||||
|
||||
def capture_preview(self):
|
||||
capture = gp.check_result(gp.gp_camera_capture_preview(self.inner))
|
||||
file_data = gp.check_result(gp.gp_file_get_data_and_size(capture))
|
||||
data = memoryview(file_data)
|
||||
image = Image.open(io.BytesIO(file_data))
|
||||
image.save("src/nenuscanner/static/feed.jpg")
|
||||
|
||||
def save(self, capture, output_file):
|
||||
preview = self.inner.file_get(capture.folder, capture.name[:-3] + 'JPG', gp.GP_FILE_TYPE_NORMAL)
|
||||
raw = self.inner.file_get(capture.folder, capture.name, gp.GP_FILE_TYPE_RAW)
|
||||
preview.save(output_file + '.jpg')
|
||||
# Resize preview
|
||||
subprocess.run(['convert', output_file + '.jpg', '-resize', '10%', output_file + '.jpg'])
|
||||
raw.save(output_file + '.cr2')
|
||||
|
||||
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")
|
||||
|
||||
if was_entered:
|
||||
self.__enter__()
|
||||
|
||||
# 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):
|
||||
self.leds = leds
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
def capture(self):
|
||||
# Find which leds are turned on
|
||||
found = None
|
||||
all_on = False
|
||||
for led in self.leds.leds:
|
||||
if led.is_on:
|
||||
if found is None:
|
||||
found = led
|
||||
else:
|
||||
all_on = True
|
||||
|
||||
if all_on:
|
||||
return 'data-keep/small/all_on.jpg'
|
||||
elif found is not None:
|
||||
return 'data-keep/small/' + str(found) + '.jpg'
|
||||
else:
|
||||
return 'data-keep/small/all_off.jpg'
|
||||
|
||||
def save(self, capture, output_file):
|
||||
shutil.copyfile(capture, output_file + '.jpg')
|
||||
|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from os.path import join
|
||||
|
||||
DATA_DIR = 'data'
|
||||
BACKUPS_DIR = 'data-backups'
|
||||
CALIBRATION_DIR = join(DATA_DIR, 'calibrations')
|
||||
OBJECT_DIR = join(DATA_DIR, 'objects')
|
||||
DATABASE_PATH = join(DATA_DIR, 'db.sqlite')
|
||||
|
||||
AUTO_USE_LAST_CALIBRATION = True
|
||||
DELAY = None
|
||||
GPIO_CHIP = 'gpiochip0'
|
||||
LEDS_UUIDS = [17, 18, 22, 23, 24, 27]
|
||||
CAMERA = 'real'
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from os.path import join
|
||||
|
||||
DATA_DIR = 'data'
|
||||
BACKUPS_DIR = 'data-backups'
|
||||
CALIBRATION_DIR = join(DATA_DIR, 'calibrations')
|
||||
OBJECT_DIR = join(DATA_DIR, 'objects')
|
||||
DATABASE_PATH = join(DATA_DIR, 'db.sqlite')
|
||||
|
||||
AUTO_USE_LAST_CALIBRATION = False
|
||||
DELAY = 0.5
|
||||
GPIO_CHIP = None
|
||||
LEDS_UUIDS = [17, 18, 22, 23, 24, 27]
|
||||
CAMERA = 'dummy'
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
from enum import IntEnum
|
||||
from datetime import datetime
|
||||
from flask import g
|
||||
import itertools
|
||||
import os
|
||||
from os.path import join
|
||||
import shutil
|
||||
|
|
@ -171,28 +172,37 @@ class Acquisition:
|
|||
def get_pretty_date(self) -> str:
|
||||
return dateutils.format(self.date)
|
||||
|
||||
@staticmethod
|
||||
def delete_from_id(acquisition_id: int, db: sqlite3.Connection) -> 'Acquisition':
|
||||
cur = db.cursor()
|
||||
response = cur.execute(
|
||||
'DELETE FROM acquisition WHERE id = ? RETURNING ' + Acquisition.select_args() + ';',
|
||||
[acquisition_id]
|
||||
)
|
||||
return Acquisition.from_row(response.fetchone())
|
||||
|
||||
class FullObject:
|
||||
def __init__(self, object_id: int, name: str, acquisitions: list[Acquisition]):
|
||||
self.id = object_id
|
||||
|
||||
class Project:
|
||||
def __init__(self, name: str, objects: list['Object']):
|
||||
self.name = name
|
||||
self.acquisitions = acquisitions
|
||||
self.objects = objects
|
||||
|
||||
|
||||
class Object:
|
||||
@staticmethod
|
||||
def select_args() -> str:
|
||||
return 'id, name'
|
||||
return 'id, name, project'
|
||||
|
||||
def __init__(self, object_id: int, name: str):
|
||||
def __init__(self, object_id: int, name: str, project: str):
|
||||
self.id = object_id
|
||||
self.name = name
|
||||
self.project = project
|
||||
|
||||
def save(self, db: sqlite3.Connection):
|
||||
cur = db.cursor()
|
||||
cur.execute(
|
||||
'UPDATE object SET name = ? WHERE id = ?',
|
||||
[self.name, self.id]
|
||||
'UPDATE object SET name = ?, project = ? WHERE id = ?',
|
||||
[self.name, self.project, self.id]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -202,11 +212,11 @@ class Object:
|
|||
return Object(*row)
|
||||
|
||||
@staticmethod
|
||||
def create(name: str, db: sqlite3.Connection) -> 'Object':
|
||||
def create(name: str, project: str, db: sqlite3.Connection) -> 'Object':
|
||||
cur = db.cursor()
|
||||
response = cur.execute(
|
||||
'INSERT INTO object(name) VALUES (?) RETURNING ' + Object.select_args() + ';',
|
||||
[name]
|
||||
'INSERT INTO object(name, project) VALUES (?, ?) RETURNING ' + Object.select_args() + ';',
|
||||
[name, project]
|
||||
)
|
||||
object = Object.from_row(response.fetchone())
|
||||
os.makedirs(join(config.OBJECT_DIR, str(object.id)))
|
||||
|
|
@ -230,6 +240,12 @@ class Object:
|
|||
)
|
||||
return list(map(Object.from_row, response.fetchall()))
|
||||
|
||||
@staticmethod
|
||||
def all_by_project(db: sqlite3.Connection) -> list[Project]:
|
||||
objects = [x.full(db) for x in Object.all(db)]
|
||||
objects_by_projects = itertools.groupby(objects, lambda x: x.project)
|
||||
return list(map(lambda x: Project(x[0], list(x[1])), objects_by_projects))
|
||||
|
||||
def add_acquisition(self, calibration_id: int, db: sqlite3.Connection) -> Acquisition:
|
||||
cur = db.cursor()
|
||||
response = cur.execute(
|
||||
|
|
@ -238,14 +254,32 @@ class Object:
|
|||
)
|
||||
return Acquisition.from_row(response.fetchone())
|
||||
|
||||
def full(self, db: sqlite3.Connection) -> FullObject:
|
||||
@staticmethod
|
||||
def delete_from_id(object_id: int, db: sqlite3.Connection) -> 'Object':
|
||||
cur = db.cursor()
|
||||
response = cur.execute(
|
||||
'DELETE FROM object WHERE id = ? RETURNING ' + Object.select_args() + ';',
|
||||
[object_id]
|
||||
)
|
||||
return Object.from_row(response.fetchone())
|
||||
|
||||
def full(self, db: sqlite3.Connection) -> 'FullObject':
|
||||
cur = db.cursor()
|
||||
response = cur.execute(
|
||||
'SELECT ' + Acquisition.select_args() + ' FROM acquisition WHERE object_id = ? ORDER BY date DESC;',
|
||||
[self.id]
|
||||
)
|
||||
acquisitions = list(map(lambda x: Acquisition.from_row(x), response.fetchall()))
|
||||
return FullObject(self.id, self.name, acquisitions)
|
||||
return FullObject(self.id, self.name, self.project, acquisitions)
|
||||
|
||||
|
||||
class FullObject(Object):
|
||||
def __init__(self, object_id: int, name: str, project: str, acquisitions: list[Acquisition]):
|
||||
super().__init__(object_id, name, project)
|
||||
self.id = object_id
|
||||
self.name = name
|
||||
self.project = project
|
||||
self.acquisitions = acquisitions
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -268,28 +302,6 @@ def main():
|
|||
db.row_factory = sqlite3.Row
|
||||
init(db)
|
||||
|
||||
# Create a new object
|
||||
# with db:
|
||||
# Object.create('Mon premier objet', db)
|
||||
# # calibration = Calibration.create(db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # calibration = Calibration.create(db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
|
||||
# Object.create('Mon deuxième objet', db)
|
||||
# # calibration = Calibration.create(db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # calibration = Calibration.create(db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
# # object.add_acquisition(calibration.id, db)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import gpiod
|
||||
|
||||
from . import config
|
||||
|
||||
|
||||
class Leds:
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
def on(self):
|
||||
pass
|
||||
|
||||
def off(self):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
class GpioLed:
|
||||
def __init__(self, gpio_pin: int):
|
||||
self.gpio_pin = gpio_pin
|
||||
self.led = None
|
||||
|
||||
def enter(self, chip: gpiod.Chip):
|
||||
self.led = chip.get_line(self.gpio_pin)
|
||||
self.led.request(consumer=str(self), type=gpiod.LINE_REQ_DIR_OUT)
|
||||
self.off()
|
||||
|
||||
def exit(self):
|
||||
self.off()
|
||||
self.led.release()
|
||||
self.led = None
|
||||
|
||||
def on(self):
|
||||
self.led.set_value(0)
|
||||
|
||||
def off(self):
|
||||
self.led.set_value(1)
|
||||
|
||||
def __str__(self):
|
||||
return f'LED{self.gpio_pin:02}'
|
||||
|
||||
|
||||
class GpioLeds(Leds):
|
||||
def __init__(self, chip: str, gpio_pins: list[int]):
|
||||
self._entered = False
|
||||
self.chip = gpiod.Chip(chip)
|
||||
self.leds = []
|
||||
|
||||
for pin in gpio_pins:
|
||||
self.leds.append(GpioLed(pin))
|
||||
|
||||
def __enter__(self):
|
||||
if not self._entered:
|
||||
self._entered = True
|
||||
for led in self.leds:
|
||||
led.enter(self.chip)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
# for led in self.leds:
|
||||
# led.exit()
|
||||
# self._entered = False
|
||||
pass
|
||||
|
||||
def off(self):
|
||||
for led in self.leds:
|
||||
led.off()
|
||||
|
||||
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):
|
||||
self.gpio_pin = gpio_pin
|
||||
self.is_on = False
|
||||
|
||||
def enter(self):
|
||||
pass
|
||||
|
||||
def exit(self):
|
||||
pass
|
||||
|
||||
def on(self):
|
||||
self.is_on = True
|
||||
|
||||
def off(self):
|
||||
self.is_on = False
|
||||
|
||||
def __str__(self):
|
||||
return f'LED{self.gpio_pin:02}'
|
||||
|
||||
|
||||
class DummyLeds(Leds):
|
||||
def __init__(self, gpio_pins: list[int]):
|
||||
self.leds = []
|
||||
for pin in gpio_pins:
|
||||
self.leds.append(DummyLed(pin))
|
||||
|
||||
def __enter__(self):
|
||||
for led in self.leds:
|
||||
led.enter()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
for led in self.leds:
|
||||
led.exit()
|
||||
|
||||
def off(self):
|
||||
for led in self.leds:
|
||||
led.off()
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import subprocess
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
from .. import db
|
||||
from . import object, calibration, acquisition, camera, leds
|
||||
|
||||
blueprint = Blueprint('routes', __name__)
|
||||
|
||||
|
||||
# Generic routes
|
||||
@blueprint.route("/")
|
||||
def index():
|
||||
"""
|
||||
Serves the index of nenuscanner.
|
||||
"""
|
||||
conn = db.get()
|
||||
projects = db.Object.all_by_project(conn)
|
||||
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(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,104 @@
|
|||
from flask import Blueprint, Response, render_template, session, redirect
|
||||
from os.path import join
|
||||
|
||||
from .. import db, config, scanner
|
||||
|
||||
blueprint = Blueprint('acquisition', __name__)
|
||||
|
||||
|
||||
@blueprint.route('/scan/<id>')
|
||||
def scan(id: int):
|
||||
"""
|
||||
Route to scan an object
|
||||
"""
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
object = db.Object.get_from_id(id, conn)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
return render_template('scan.html', object=object, calibrated=True)
|
||||
|
||||
|
||||
@blueprint.route('/rescan/<id>')
|
||||
def scan_existing(id: int):
|
||||
conn = db.get()
|
||||
calibrated = session.get('calibration_id', None) is not None
|
||||
acquisition = db.Acquisition.get_from_id(id, conn)
|
||||
object = acquisition.object(conn) if acquisition is not None else None
|
||||
return render_template('scan.html', object=object, acquisition=acquisition, calibrated=calibrated)
|
||||
|
||||
|
||||
@blueprint.route('/run/<object_id>')
|
||||
def run(object_id: int):
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
object = db.Object.get_from_id(object_id, conn)
|
||||
|
||||
if object is None:
|
||||
raise RuntimeError(f"Aucun objet d'id {object_id}")
|
||||
|
||||
with conn:
|
||||
acquisition = object.add_acquisition(calibration_id, conn)
|
||||
|
||||
def generate():
|
||||
yield str(acquisition.id)
|
||||
for value in scanner.scan(join(config.OBJECT_DIR, str(object.id), str(acquisition.id))):
|
||||
yield value
|
||||
|
||||
return Response(generate(), mimetype='text/plain')
|
||||
|
||||
|
||||
@blueprint.route('/rerun/<acquisition_id>')
|
||||
def rescan(acquisition_id: int):
|
||||
"""
|
||||
Route to relaunch an acquisition
|
||||
"""
|
||||
conn = db.get()
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
|
||||
if calibration_id is None:
|
||||
raise RuntimeError("Impossible de faire l'acquisition sans étalonnage")
|
||||
|
||||
acquisition = db.Acquisition.get_from_id(acquisition_id, conn)
|
||||
|
||||
if acquisition is None:
|
||||
raise RuntimeError(f"Aucun acquisition d'id {acquisition_id}")
|
||||
|
||||
object = acquisition.object(conn)
|
||||
|
||||
def generate():
|
||||
for value in scanner.scan(join(config.OBJECT_DIR, str(object.id), str(acquisition.id))):
|
||||
yield value
|
||||
|
||||
return Response(generate(), mimetype='text/plain')
|
||||
|
||||
|
||||
@blueprint.route('/validate/<acquisition_id>')
|
||||
def validate(acquisition_id: int):
|
||||
conn = db.get()
|
||||
acquisition = db.Acquisition.get_from_id(acquisition_id, conn)
|
||||
|
||||
if acquisition is None:
|
||||
raise Exception(f"Aucune acquisition d'id {acquisition_id}")
|
||||
|
||||
object = acquisition.object(conn)
|
||||
|
||||
acquisition.validated = True
|
||||
with conn:
|
||||
acquisition.save(conn)
|
||||
|
||||
return redirect(f'/object/{object.id}')
|
||||
|
||||
|
||||
@blueprint.route("/delete/<acquisition_id>")
|
||||
def delete(acquisition_id: int):
|
||||
conn = db.get()
|
||||
with conn:
|
||||
acqusition = db.Acquisition.delete_from_id(acquisition_id, conn)
|
||||
return redirect('/object/' + str(acqusition.object_id))
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
from flask import Blueprint, Response, render_template, redirect, session
|
||||
from os.path import join
|
||||
import json
|
||||
|
||||
from .. import db, utils, scanner, config, calibration
|
||||
|
||||
blueprint = Blueprint('calibration', __name__)
|
||||
|
||||
|
||||
@blueprint.route("/create")
|
||||
def create():
|
||||
"""
|
||||
Creates a new calibration and redirects to the page to calibrate.
|
||||
"""
|
||||
conn = db.get()
|
||||
with conn:
|
||||
calibration = db.Calibration.create(conn)
|
||||
session['calibration_id'] = calibration.id
|
||||
|
||||
return redirect('/calibration/calibrate')
|
||||
|
||||
|
||||
@blueprint.route("/calibrate")
|
||||
def calibrate():
|
||||
"""
|
||||
Returns the page to calibrate the system.
|
||||
"""
|
||||
conn = db.get()
|
||||
if 'calibration_id' not in session:
|
||||
with conn:
|
||||
calibration = db.Calibration.create(conn)
|
||||
session['calibration_id'] = calibration.id
|
||||
else:
|
||||
calibration = db.Calibration.get_from_id(session['calibration_id'], conn)
|
||||
|
||||
if calibration.state in [db.CalibrationState.Empty, db.CalibrationState.HasData]:
|
||||
return render_template('calibrate.html')
|
||||
else:
|
||||
return render_template('calibration.html', calibration=calibration)
|
||||
|
||||
|
||||
@blueprint.route('/scan')
|
||||
def scan():
|
||||
"""
|
||||
Starts a scan for calibration.
|
||||
"""
|
||||
conn = db.get()
|
||||
|
||||
if 'calibration_id' not in session:
|
||||
with conn:
|
||||
calibration = db.Calibration.create(conn)
|
||||
calibration_id = str(calibration.id)
|
||||
session['calibration_id'] = calibration.id
|
||||
else:
|
||||
calibration_id = str(session['calibration_id'])
|
||||
calibration = utils.get_calibration(conn)
|
||||
|
||||
def generate():
|
||||
for value in scanner.scan(join(config.CALIBRATION_DIR, calibration_id), False):
|
||||
yield value
|
||||
|
||||
with conn:
|
||||
calibration.state = db.CalibrationState.HasData
|
||||
calibration.save(conn)
|
||||
|
||||
return Response(generate(), mimetype='text/plain')
|
||||
|
||||
|
||||
@blueprint.route('/compute')
|
||||
def compute():
|
||||
"""
|
||||
Compute the calibration from the scan.
|
||||
"""
|
||||
conn = db.get()
|
||||
id = session['calibration_id']
|
||||
calib = db.Calibration.get_from_id(id, conn)
|
||||
if calib is None:
|
||||
return 'oops', 404
|
||||
|
||||
calibration_json = calibration.calibrate(join(config.CALIBRATION_DIR, str(id)))
|
||||
with open(join(config.CALIBRATION_DIR, str(id), 'calibration.json'), 'w') as f:
|
||||
json.dump(calibration_json, f, indent=4)
|
||||
with conn:
|
||||
calib.state = db.CalibrationState.IsComputed
|
||||
calib.save(conn)
|
||||
|
||||
return 'ok'
|
||||
|
||||
|
||||
@blueprint.route('/cancel')
|
||||
def cancel():
|
||||
"""
|
||||
Cancels a calibration.
|
||||
"""
|
||||
conn = db.get()
|
||||
calibration = db.Calibration.get_from_id(session['calibration_id'], conn)
|
||||
calibration.state = db.CalibrationState.HasData
|
||||
with conn:
|
||||
calibration.save(conn)
|
||||
return redirect('/calibration/calibrate')
|
||||
|
||||
|
||||
@blueprint.route('/validate')
|
||||
def validate():
|
||||
"""
|
||||
Validates a calibration.
|
||||
"""
|
||||
conn = db.get()
|
||||
calib = utils.get_calibration(conn)
|
||||
if calib is None:
|
||||
return 'oops', 404
|
||||
|
||||
with conn:
|
||||
calib.validate(conn)
|
||||
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@blueprint.route('/use-last')
|
||||
def use_last():
|
||||
"""
|
||||
Sets the current calibration to the last one that was validated.
|
||||
"""
|
||||
conn = db.get()
|
||||
calib = db.Calibration.get_last(conn)
|
||||
session['calibration_id'] = calib.id
|
||||
return redirect('/calibration/calibrate')
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
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', methods=['GET'])
|
||||
def camera_feed():
|
||||
capture_preview()
|
||||
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:
|
||||
with C.get() as cam:
|
||||
cam.capture_preview()
|
||||
return jsonify({'status': 'ok'})
|
||||
except C.CameraException as e:
|
||||
return jsonify({'status': 'error', 'error': str(e)}), 500
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
from flask import Blueprint, render_template, request, send_file, jsonify, session, current_app
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
import gpiod
|
||||
|
||||
from .. import camera as C
|
||||
from .. import leds,config
|
||||
|
||||
blueprint = Blueprint('leds', __name__)
|
||||
|
||||
# Routes for object management
|
||||
|
||||
@blueprint.route('/')
|
||||
def get():
|
||||
"""
|
||||
Returns the pages showing all leds.
|
||||
"""
|
||||
|
||||
return render_template(
|
||||
'leds.html', leds= config.LEDS_UUIDS)
|
||||
|
||||
|
||||
@blueprint.route('/set', methods=['POST'])
|
||||
def set_led():
|
||||
"""
|
||||
Reçoit une commande pour allumer ou éteindre une LED.
|
||||
Attend un JSON : { "led": "14", "state": "on" } ou { "led": "15, "state": "off" }
|
||||
"""
|
||||
data = request.get_json()
|
||||
led = data.get('led')
|
||||
state = data.get('state')
|
||||
# get the controller (lazy, stored on app.extensions)
|
||||
gpio_leds = leds.get()
|
||||
|
||||
try:
|
||||
# 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))
|
||||
print(f"Setting {led} / {gpio_led} to {state}")
|
||||
if state == "on":
|
||||
gpio_led.on()
|
||||
else:
|
||||
gpio_led.off()
|
||||
|
||||
except Exception as e:
|
||||
raise
|
||||
print(f'{e}')
|
||||
return jsonify({'status': 'error', 'error': 'error'}), 400
|
||||
|
||||
|
||||
print(f"Commande reçue pour {led} : {state}")
|
||||
|
||||
return jsonify({'status': 'ok', 'led': led, 'state': state})
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
from flask import Blueprint, render_template, redirect, request
|
||||
import os
|
||||
from os.path import join
|
||||
import itertools
|
||||
|
||||
from .. import db, config, archive
|
||||
|
||||
blueprint = Blueprint('routes', __name__)
|
||||
|
||||
|
||||
# Routes for object management
|
||||
@blueprint.route('/<id>')
|
||||
def get(id: int):
|
||||
"""
|
||||
Returns the page showing an object.
|
||||
"""
|
||||
conn = db.get()
|
||||
object = db.Object.get_from_id(id, conn).full(conn)
|
||||
return render_template('object.html', object=object)
|
||||
|
||||
|
||||
@blueprint.route('/create', methods=['POST'])
|
||||
def create():
|
||||
"""
|
||||
Creates a new object.
|
||||
"""
|
||||
conn = db.get()
|
||||
with conn:
|
||||
db.Object.create(request.form.get('name'), request.form.get('project'), conn)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@blueprint.route('/delete/<id>')
|
||||
def delete(id: int):
|
||||
"""
|
||||
Deletes an object from its id.
|
||||
"""
|
||||
conn = db.get()
|
||||
with conn:
|
||||
db.Object.delete_from_id(id, conn)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
def download_object(id: int, archive: archive.ArchiveSender):
|
||||
"""
|
||||
Helper for routes that send archives.
|
||||
"""
|
||||
conn = db.get()
|
||||
object = db.Object.get_from_id(id, conn).full(conn)
|
||||
|
||||
# Group acquisitions sharing calibration
|
||||
def keyfunc(x: db.Calibration) -> int:
|
||||
return x.calibration_id
|
||||
|
||||
acquisitions_sorted = sorted(object.acquisitions, key=keyfunc)
|
||||
acquisitions_grouped = [
|
||||
(db.Calibration.get_from_id(k, conn), list(g))
|
||||
for k, g in itertools.groupby(acquisitions_sorted, key=keyfunc)
|
||||
]
|
||||
|
||||
# Create archive file to send
|
||||
for calibration_index, (calib, acquisitions) in enumerate(acquisitions_grouped):
|
||||
calibration_dir = join(config.CALIBRATION_DIR, str(calib.id))
|
||||
|
||||
# Add calibration images
|
||||
for image in os.listdir(calibration_dir):
|
||||
archive.add_file(
|
||||
f'object/{calibration_index}/calibration/{image}',
|
||||
join(calibration_dir, image)
|
||||
)
|
||||
|
||||
# Add each acquisition
|
||||
for acquisition_index, acquisition in enumerate(acquisitions):
|
||||
acquisition_dir = join(config.OBJECT_DIR, str(object.id), str(acquisition.id))
|
||||
|
||||
for image in os.listdir(acquisition_dir):
|
||||
archive.add_file(
|
||||
f'object/{calibration_index}/{acquisition_index}/{image}',
|
||||
join(acquisition_dir, image)
|
||||
)
|
||||
|
||||
return archive.response()
|
||||
|
||||
|
||||
@blueprint.route('/download/tar/<id>')
|
||||
def download_object_tar(id: int):
|
||||
"""
|
||||
Downloads an object as a tar archive.
|
||||
"""
|
||||
return download_object(id, archive.TarSender())
|
||||
|
||||
|
||||
@blueprint.route('/download/zip/<id>')
|
||||
def download_object_zip(id: int):
|
||||
"""
|
||||
Downloads an object as a zip archive.
|
||||
"""
|
||||
return download_object(id, archive.ZipSender())
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import os
|
||||
from os.path import join
|
||||
import time
|
||||
from . import leds, camera, config
|
||||
|
||||
|
||||
def delay_capture(cam):
|
||||
# Measure the time it takes to capture
|
||||
start = time.time()
|
||||
output = cam.capture()
|
||||
delta = time.time() - start
|
||||
|
||||
# Wait for at least one second between each capture
|
||||
if config.DELAY is not None and delta < config.DELAY:
|
||||
time.sleep(config.DELAY - delta)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def delay_save(cam, source, target):
|
||||
# Measure the time it takes to save
|
||||
start = time.time()
|
||||
cam.save(source, target)
|
||||
delta = time.time() - start
|
||||
|
||||
# Wait for at least one second between each save
|
||||
if config.DELAY is not None and delta < config.DELAY:
|
||||
time.sleep(config.DELAY - delta)
|
||||
|
||||
|
||||
def scan(output_dir: str, on_and_off: bool = True):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
file_paths = []
|
||||
length = len(config.LEDS_UUIDS) + (2 if on_and_off else 0)
|
||||
|
||||
with camera.get() as cam:
|
||||
with leds.get() as gpio_leds:
|
||||
for count, led in enumerate(gpio_leds.leds):
|
||||
print(f'Turn on {led}')
|
||||
|
||||
led.on()
|
||||
file_paths.append((str(led), delay_capture(cam)))
|
||||
led.off()
|
||||
|
||||
print(f'Turn off {led}')
|
||||
|
||||
ratio = (count + 1) / (2 * length)
|
||||
yield f'{{ "status": "captured", "id": "{led}", "ratio": {ratio:.3} }}\n'
|
||||
|
||||
# capture with all leds ON OFF
|
||||
if on_and_off:
|
||||
gpio_leds.on()
|
||||
file_paths.append(('all_on', delay_capture(cam)))
|
||||
ratio = (length - 1) / (2 * length)
|
||||
yield f'{{ "status": "captured", "id": "all_on", "ratio": {ratio:.3} }}\n'
|
||||
|
||||
gpio_leds.off()
|
||||
file_paths.append(('all_off', delay_capture(cam)))
|
||||
ratio = 0.5
|
||||
yield f'{{ "status": "captured", "id": "all_off", "ratio": {ratio:.3} }}\n'
|
||||
|
||||
with camera.get() as cam:
|
||||
for count, (target, source) in enumerate(file_paths):
|
||||
delay_save(cam, source, join(output_dir, target))
|
||||
ratio = 0.5 + (count + 1) / (2 * length)
|
||||
yield f'{{ "status": "ready", "id": "{target}", "ratio": {ratio:.3} }}\n'
|
||||
|
|
@ -15,10 +15,11 @@ CREATE TABLE acquisition (
|
|||
date INTEGER NOT NULL,
|
||||
validated INT NOT NULL,
|
||||
CONSTRAINT fk_calibration FOREIGN KEY(calibration_id) REFERENCES calibration(id),
|
||||
CONSTRAINT fk_object FOREIGN KEY(object_id) REFERENCES object(id)
|
||||
CONSTRAINT fk_object FOREIGN KEY(object_id) REFERENCES object(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE object (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
|
@ -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>
|
||||
|
|
@ -21,26 +23,31 @@
|
|||
</a>
|
||||
</div>
|
||||
<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">
|
||||
<a id="calibration-tag-0" class="calibration-tag navbar-item" href="/calibrate/" {% if calibration.state != 0 %}style="display: none;"{% endif %}>
|
||||
<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 %}>
|
||||
<span id="calibration-tag-0" class="tags has-addons">
|
||||
<span class="tag is-dark">étalonnage</span>
|
||||
<span class="tag is-danger">aucune donnée</span>
|
||||
</span>
|
||||
</a>
|
||||
<a id="calibration-tag-1" class="calibration-tag navbar-item" href="/calibrate/" {% if calibration.state != 1 %}style="display: none;"{% endif %}>
|
||||
<a id="calibration-tag-1" class="calibration-tag navbar-item" href="/calibration/calibrate" {% if calibration.state != 1 %}style="display: none;"{% endif %}>
|
||||
<span class="tags has-addons" >
|
||||
<span class="tag is-dark">étalonnage</span>
|
||||
<span class="tag is-warning">non calculé</span>
|
||||
</span>
|
||||
</a>
|
||||
<a id="calibration-tag-2" class="calibration-tag navbar-item" href="/calibrate/" {% if calibration.state != 2 %}style="display: none;"{% endif %}>
|
||||
<a id="calibration-tag-2" class="calibration-tag navbar-item" href="/calibration/calibrate" {% if calibration.state != 2 %}style="display: none;"{% endif %}>
|
||||
<span class="tags has-addons">
|
||||
<span class="tag is-dark">étalonnage</span>
|
||||
<span class="tag is-warning">non validé</span>
|
||||
</span>
|
||||
</a>
|
||||
<a id="calibration-tag-3" class="calibration-tag navbar-item" href="/calibrate" {% if calibration.state != 3 %}style="display: none;"{% endif %}>
|
||||
<a id="calibration-tag-3" class="calibration-tag navbar-item" href="/calibration/calibrate" {% if calibration.state != 3 %}style="display: none;"{% endif %}>
|
||||
<span class="tags has-addons">
|
||||
<span class="tag is-dark">étalonnage</span>
|
||||
<span class="tag is-success">validé le {{ calibration.get_pretty_short_date() }}</span>
|
||||
|
|
@ -5,25 +5,20 @@
|
|||
<div class="container">
|
||||
<h1 class="title">Étalonnage</h1>
|
||||
<div class="mb-2">
|
||||
<p>Placez la mire devant le scanner puis appuyez sur le bouton pour prévisualiser ou étalonner le scanner.</p>
|
||||
<p>Placez la mire devant le scanner puis appuyez sur le bouton étalonner le scanner.</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<button id="preview-button" class="button is-link">Prévisualiser</button>
|
||||
<button id="scan-button" class="button is-link">Acquérir les données d'étalonnage</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="scan-button" class="button is-link">Acquérir les données d'étalonnage</button>
|
||||
<a href="/calibration/use-last" class="button is-link">Réutiliser le dernier étalonnage</a>
|
||||
</div>
|
||||
</div>
|
||||
<article id="error-container" class="message is-danger" style="display: none;">
|
||||
<div id="error-content" class="message-body">
|
||||
</div>
|
||||
</article>
|
||||
<div class="columns is-desktop">
|
||||
<div class="column is-offset-4 is-4">
|
||||
<img id="preview-image" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed-grid has-8-cols">
|
||||
<div id="grid" class="grid">
|
||||
|
||||
|
|
@ -42,36 +37,14 @@
|
|||
<script>
|
||||
let scanIndex = 1;
|
||||
let progress = 0;
|
||||
let previewButton = document.getElementById('preview-button');
|
||||
let scanButton = document.getElementById('scan-button');
|
||||
let progressBar = document.getElementById('progress-bar');
|
||||
let previewImage = document.getElementById('preview-image');
|
||||
let grid = document.getElementById('grid');
|
||||
let buttons = [previewButton, scanButton];
|
||||
let errorContainer = document.getElementById('error-container');
|
||||
let errorContent = document.getElementById('error-content');
|
||||
let calibrateDiv = document.getElementById('calibrate');
|
||||
let calibrateButton = document.getElementById('calibrate-button');
|
||||
|
||||
previewButton.addEventListener('click', async () => {
|
||||
buttons.forEach(x => x.setAttribute('disabled', 'disabled'));
|
||||
previewButton.classList.add('is-loading');
|
||||
let response = await fetch('/api/preview/{{ calibration.id }}');
|
||||
let uuid = await response.text();
|
||||
|
||||
if (response.status >= 200 && response.status < 400) {
|
||||
previewImage.src = "/data/{{ calibration.id }}/previews/" + uuid + ".jpg";
|
||||
previewImage.style.display = "block";
|
||||
errorContainer.style.display = "none";
|
||||
} else {
|
||||
errorContainer.style.display = "block";
|
||||
errorContent.innerHTML = uuid;
|
||||
}
|
||||
|
||||
previewButton.classList.remove('is-loading');
|
||||
buttons.forEach(x => x.removeAttribute('disabled'));
|
||||
});
|
||||
|
||||
// If we already have calibration images, we show them right now
|
||||
if ({% if calibration.state > 0 %}true{% else %}false{% endif %}) {
|
||||
let cell, img;
|
||||
|
|
@ -90,20 +63,19 @@
|
|||
scanIndex++;
|
||||
progress = 0;
|
||||
progressBar.value = 0;
|
||||
previewImage.style.display = "none";
|
||||
buttons.forEach(x => x.setAttribute('disabled', 'disabled'));
|
||||
scanButton.setAttribute('disabled', 'disabled');
|
||||
scanButton.classList.add('is-loading');
|
||||
progressBar.style.display = "block";
|
||||
grid.innerHTML = '';
|
||||
|
||||
let response = await fetch('/api/scan-for-calibration');
|
||||
let response = await fetch('/calibration/scan');
|
||||
let reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
|
||||
while (true) {
|
||||
let { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
buttons.forEach(x => x.removeAttribute('disabled'));
|
||||
scanButton.removeAttribute('disabled');
|
||||
scanButton.classList.remove('is-loading');
|
||||
calibrateDiv.style.display = "block";
|
||||
|
||||
|
|
@ -114,22 +86,31 @@
|
|||
break;
|
||||
}
|
||||
|
||||
let [ uuid, ratio ] = value.trim().split(',');
|
||||
progress = Math.ceil(1000 * parseFloat(ratio));
|
||||
let cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
let img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/calibrations/{{ calibration.id }}/' + uuid + '.jpg?v=' + scanIndex;
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
for (let line of value.split('\n')) {
|
||||
if (line === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let { status, id, ratio } = JSON.parse(line);
|
||||
progress = Math.ceil(1000 * parseFloat(ratio));
|
||||
|
||||
if (status === "ready") {
|
||||
let cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
let img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/calibrations/{{ calibration.id }}/' + id + '.jpg?v=' + scanIndex;
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calibrateButton.addEventListener('click', async () => {
|
||||
calibrateButton.classList.add('is-loading');
|
||||
|
||||
await fetch('/api/calibrate');
|
||||
await fetch('/calibration/compute');
|
||||
window.location.reload();
|
||||
|
||||
});
|
||||
|
|
@ -51,14 +51,14 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="/new-calibration" class="button is-link">Créer un nouvel étalonnage</a>
|
||||
<a href="/calibration/create" class="button is-link">Créer un nouvel étalonnage</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="control">
|
||||
<a href="/cancel-calibration" class="button">Retourner à la page d'acquisition</a>
|
||||
<a href="/calibration/cancel" class="button">Retourner à la page d'acquisition</a>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="/validate-calibration" class="button is-link">Valider l'étalonnage</a>
|
||||
<a href="/calibration/validate" class="button is-link">Valider l'étalonnage</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
{% 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" 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());
|
||||
}
|
||||
|
||||
const img = document.getElementById('camera-preview');
|
||||
img.onload = function() {
|
||||
requestAnimationFrame(refreshPreview);
|
||||
}
|
||||
|
||||
function refreshPreview() {
|
||||
const url = '/camera/feed.jpg?' + new Date().getTime();
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// When ready, load preview
|
||||
refreshPreview();
|
||||
});
|
||||
|
||||
});
|
||||
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,146 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block extracss %}
|
||||
<style>
|
||||
.project {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Bienvenue sur NenuScanner</h1>
|
||||
{% if projects %}
|
||||
<div class="content">
|
||||
<p>Voici les projets existants dans la base de données :
|
||||
<div class="fixed-grid has-6-cols">
|
||||
<div class="grid">
|
||||
{% for project in projects %}
|
||||
<a href="#" id="project-{{ loop.index0 }}" class="cell p-2 has-text-centered has-text-white-dark" style="border: solid; border-width: 1px; border-radius: 5px;">
|
||||
<div>
|
||||
<strong>{{ project.name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<em>({{ project.objects | length }} objets)</em>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% for project in projects %}
|
||||
<div class="project project-{{ loop.index0 }}">
|
||||
<h2>{{ project.name }} <em>({{ project.objects | length }} objets)</em></h2>
|
||||
<div class="fixed-grid has-6-cols mb-5">
|
||||
<div class="grid">
|
||||
{% for object in project.objects %}
|
||||
<a href="/object/{{ object.id }}" class="cell p-2 has-text-centered has-text-white-dark" style="border: solid; border-width: 1px; border-radius: 5px;">
|
||||
<div>
|
||||
<strong>{{ object.name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
{% if object.acquisitions %}
|
||||
<img src="/data/objects/{{ object.id }}/{{ object.acquisitions[0].id }}/{{ leds[0] }}.jpg">
|
||||
|
||||
{% else %}
|
||||
<em>Pas d'acquisitions</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Il n'y a aucun projet pour le moment...</p>
|
||||
{% endif %}
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button id="add-object" class="button is-link">Ajouter un nouvel objet</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-object-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<form action="/object/create" method="POST">
|
||||
<div class="modal-content">
|
||||
<div class="field">
|
||||
<label class="label has-text-white-dark">Nom du projet</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="project" placeholder="Nom du projet" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label has-text-white-dark">Nom de l'objet</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="name" placeholder="Nom de l'objet" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<button class="button is-link">Créer un objet</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let addObjectButton = document.getElementById('add-object');
|
||||
let addObjectModal = document.getElementById('add-object-modal');
|
||||
|
||||
// Modal button show the modal
|
||||
addObjectButton.addEventListener('click', () => {
|
||||
addObjectModal.classList.add('is-active');
|
||||
});
|
||||
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach((close) => {
|
||||
close.addEventListener('click', () => {
|
||||
addObjectModal.classList.remove('is-active');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let projects = document.getElementsByClassName('project');
|
||||
for (let i = 0; i < {{ projects | length }}; i++) {
|
||||
let clickedProject = document.getElementById('project-' + i);
|
||||
clickedProject.addEventListener('click', () => {
|
||||
|
||||
let same = clickedProject.classList.contains('has-text-link');
|
||||
|
||||
if (same) {
|
||||
clickedProject.classList.remove('has-text-link');
|
||||
clickedProject.classList.add('has-text-white-dark');
|
||||
} else {
|
||||
clickedProject.classList.remove('has-text-white-dark');
|
||||
clickedProject.classList.add('has-text-link');
|
||||
}
|
||||
|
||||
for (let j = 0; j < {{ projects | length }}; j++) {
|
||||
if (j !== i) {
|
||||
let otherProject = document.getElementById('project-' + j);
|
||||
otherProject.classList.remove('has-text-link');
|
||||
otherProject.classList.add('has-text-white-dark');
|
||||
}
|
||||
}
|
||||
|
||||
for (let p of projects) {
|
||||
if (p.classList.contains('project-' + i) && !same) {
|
||||
p.style.display = "block";
|
||||
} else {
|
||||
p.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Contrôler les LEDS</h1>
|
||||
|
||||
<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 %}
|
||||
<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>
|
||||
|
||||
<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 %}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container content">
|
||||
<h1 class="title">{{ object.name }}</h1>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<a class="button is-link" href="/object/download/tar/{{ object.id }}">Télécharger les données de l'objet (archive TAR)</a>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-link" href="/object/download/zip/{{ object.id }}">Télécharger les données de l'objet (archive ZIP)</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if object.acquisitions %}
|
||||
<div class="fixed-grid has-6-cols">
|
||||
<div class="grid">
|
||||
{% for acquisition in object.acquisitions %}
|
||||
<a class="cell" href="/acquisition/rescan/{{ acquisition.id }}">
|
||||
<div class="has-text-centered p-3" style="border-radius: 15px; border-width: 1px; border-color: {% if acquisition.validated %}green{% else %}red{% endif %}; border-style: solid;">
|
||||
<div>
|
||||
<img src="/data/objects/{{ object.id }}/{{ acquisition.id }}/{{ leds[0] }}.jpg">
|
||||
</div>
|
||||
<p>
|
||||
{{ acquisition.get_pretty_date() }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="field is-grouped">
|
||||
{% if calibration.state == CalibrationState.IsValidated %}
|
||||
<div class="control">
|
||||
<a href="/acquisition/scan/{{ object.id }}" class="button is-link">Faire un scan</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="control">
|
||||
<button class="button is-link" id="scan">Faire un scan</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="control">
|
||||
<button id="delete-object" class="button is-danger">Supprimer cet objet</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="upload-object" class="button is-success">Envoyer cet objet vers le serveur pour reconstruction</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if calibration.state != CalibrationState.IsValidated %}
|
||||
<div id="calibration-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="has-text-centered mb-3">Le scanner n'est pas étalonné.</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<a href="/calibration/calibrate" class="button is-link">Étalonner le scanner</a>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="use-last-calibration-button" href="/calibration/use-last" class="button is-link">Réutiliser le dernier étalonnage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="delete-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="has-text-centered mb-3">Voulez-vous vraiment supprimer l'objet ?</div>
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<button class="button custom-modal-close">Annuler</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a href="/object/delete/{{ object.id }}" class="button is-danger">Supprimer cet objet</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
document.getElementById('delete-object').addEventListener('click', () => {
|
||||
let modal = document.getElementById('delete-modal');
|
||||
modal.classList.add('is-active');
|
||||
});
|
||||
|
||||
{% if calibration.state < CalibrationState.IsValidated %}
|
||||
document.getElementById('scan').addEventListener('click', () => {
|
||||
let modal = document.getElementById('calibration-modal');
|
||||
modal.classList.add('is-active');
|
||||
});
|
||||
|
||||
document.getElementById('use-last-calibration-button').addEventListener('click', async () => {
|
||||
let resp = await fetch('/api/use-last-calibration');
|
||||
await resp.text();
|
||||
window.location.href = '/scan/{{ object.id }}';
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .custom-modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach((close) => {
|
||||
close.addEventListener('click', () => {
|
||||
for (let modal of document.querySelectorAll('#calibration-modal, #delete-modal')) {
|
||||
modal.classList.remove('is-active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title is-1">Redémarrage en cours</h1>
|
||||
<p>Merci de patienter</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>setTimeout(function(){ window.location.href = "/"; }, 2000);</script>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Faire une acquisition</h1>
|
||||
<div class="mb-2">
|
||||
<p>Placez l'objet devant le scanner puis appuyez sur le bouton pour lancer l'acquisition.</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<button {% if not calibrated or (acquisition and acquisition.validated) %}disabled{% endif %} id="scan-button" class="button is-link" {% if acquisition and acquisition.validated %}title="Vous ne pouvez pas refaire cette acquisition car elle a été validée"{% elif not calibrated %}title="Étalonnez le scanner pour pouvoir lancer l'acquisition"{% endif %}>Lancer l'acquisition</button>
|
||||
</div>
|
||||
</div>
|
||||
<article id="error-container" class="message is-danger" style="display: none;">
|
||||
<div id="error-content" class="message-body">
|
||||
</div>
|
||||
</article>
|
||||
<div class="fixed-grid has-8-cols">
|
||||
<div id="grid" class="grid">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<progress id="progress-bar" class="progress is-link" style="display: none;" value="0" max="1000"></progress>
|
||||
<div id="calibrate" {% if not acquisition %}style="display: none;"{% endif %}>
|
||||
<p>Si les données acquises conviennent, appuyez sur le bouton ci-dessous pour valider l'acquisition.</p>
|
||||
<div class="field is-grouped">
|
||||
{% if acquisition %}
|
||||
{% if acquisition.validated %}
|
||||
<div class="control">
|
||||
<button disabled id="calibrate-button" class="button is-link">Cette acquisition a déjà été validée</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="control">
|
||||
<a href="/acquisition/validate/{{ acquisition.id }}" id="calibrate-button" class="button is-link">Valider l'acquisition</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="control">
|
||||
<a id="validate-button" class="button is-link">Valider l'acquisition</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="control">
|
||||
<button id="delete-button" class="button is-danger">Supprimer l'acquisition</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="delete-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="has-text-centered mb-3">Voulez-vous vraiment supprimer l'acquisition ?</div>
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<button class="button custom-modal-close">Annuler</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a id="delete-link" {% if acquisition %} href="/acquisition/delete/{{ acquisition.id }}"{% endif %} class="button is-danger">Supprimer cette acquisition</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
let acquisitionId = {% if acquisition %}{{ acquisition.id }}{% else %}null{% endif %};
|
||||
let scanIndex = 1;
|
||||
let progress = 0;
|
||||
let scanButton = document.getElementById('scan-button');
|
||||
let progressBar = document.getElementById('progress-bar');
|
||||
let grid = document.getElementById('grid');
|
||||
let buttons = [scanButton];
|
||||
let errorContainer = document.getElementById('error-container');
|
||||
let errorContent = document.getElementById('error-content');
|
||||
let calibrateDiv = document.getElementById('calibrate');
|
||||
let validateButton = document.getElementById('validate-button');
|
||||
let deleteButton = document.getElementById('delete-button');
|
||||
let deleteLink = document.getElementById('delete-link');
|
||||
|
||||
let modal = document.getElementById('delete-modal');
|
||||
document.getElementById('delete-button').addEventListener('click', () => {
|
||||
modal.classList.add('is-active');
|
||||
});
|
||||
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .custom-modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach((close) => {
|
||||
close.addEventListener('click', () => {
|
||||
modal.classList.remove('is-active');
|
||||
});
|
||||
});
|
||||
|
||||
// If we already have calibration images, we show them right now
|
||||
if (acquisitionId !== null) {
|
||||
let cell, img;
|
||||
{% for led in leds %}
|
||||
cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/{{ led }}.jpg?v=0';
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
{% endfor %}
|
||||
|
||||
cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/all_on.jpg?v=0';
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
|
||||
cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/all_off.jpg?v=0';
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
|
||||
scanButton.addEventListener('click', async () => {
|
||||
scanIndex++;
|
||||
progress = 0;
|
||||
progressBar.value = 0;
|
||||
buttons.forEach(x => x.setAttribute('disabled', 'disabled'));
|
||||
scanButton.classList.add('is-loading');
|
||||
progressBar.style.display = "block";
|
||||
grid.innerHTML = '';
|
||||
|
||||
let response;
|
||||
if (acquisitionId === null) {
|
||||
response = await fetch('/acquisition/run/{{ object.id }}');
|
||||
} else {
|
||||
response = await fetch('/acquisition/rerun/' + acquisitionId);
|
||||
}
|
||||
|
||||
let reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
|
||||
while (true) {
|
||||
let { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
buttons.forEach(x => x.removeAttribute('disabled'));
|
||||
scanButton.classList.remove('is-loading');
|
||||
calibrateDiv.style.display = "block";
|
||||
break;
|
||||
}
|
||||
|
||||
if (acquisitionId === null) {
|
||||
acquisitionId = parseInt(value, 10);
|
||||
validateButton.setAttribute('href', '/acquisition/validate/' + acquisitionId);
|
||||
deleteLink.setAttribute('href', '/acquisition/delete/' + acquisitionId);
|
||||
window.history.pushState('', '', '/acquisition/rescan/' + acquisitionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let line of value.split('\n')) {
|
||||
if (line === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let { status, id, ratio } = JSON.parse(line);
|
||||
progress = Math.ceil(1000 * parseFloat(ratio));
|
||||
|
||||
if (status === "ready") {
|
||||
let cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
let img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/' + id + '.jpg?v=' + scanIndex;
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function refreshProgressBar() {
|
||||
if (progress !== progressBar.value) {
|
||||
progressBar.value = Math.min(progressBar.value + 5, progress);
|
||||
}
|
||||
requestAnimationFrame(refreshProgressBar);
|
||||
}
|
||||
|
||||
refreshProgressBar();
|
||||
</script>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
from flask import session
|
||||
import sqlite3
|
||||
from . import db
|
||||
|
||||
|
||||
def get_calibration(conn: sqlite3.Connection) -> db.Calibration:
|
||||
"""
|
||||
Retrieves the calibration from the session and the database.
|
||||
|
||||
Returns empty calibration if nothing is found.
|
||||
"""
|
||||
calibration_id = session.get('calibration_id', None)
|
||||
if calibration_id is None:
|
||||
return db.Calibration.Dummy
|
||||
|
||||
return db.Calibration.get_from_id(calibration_id, conn)
|
||||
|
|
@ -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
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Bienvenue sur NenuScanner</h1>
|
||||
{% if objects %}
|
||||
<div class="content">
|
||||
<p>Voici les objets existants dans la base de données :
|
||||
<ul>
|
||||
{% for object in objects %}
|
||||
<li><a href="/object/{{ object.id }}">{{ object.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Il n'y a aucun objet pour le moment...</p>
|
||||
{% endif %}
|
||||
<button id="add-object" class="button is-link">Ajouter un nouvel objet</button>
|
||||
<div id="add-object-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<form action="/create-object/" method="POST">
|
||||
<div class="modal-content">
|
||||
<div class="field">
|
||||
<label class="label">Nom de l'objet</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="name" placeholder="Nom de l'objet" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<button class="button is-link">Créer un objet</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let addObjectButton = document.getElementById('add-object');
|
||||
let addObjectModal = document.getElementById('add-object-modal');
|
||||
|
||||
// Modal button show the modal
|
||||
addObjectButton.addEventListener('click', () => {
|
||||
addObjectModal.classList.add('is-active');
|
||||
});
|
||||
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach((close) => {
|
||||
close.addEventListener('click', () => {
|
||||
addObjectModal.classList.remove('is-active');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container content">
|
||||
<h1 class="title">{{ object.name }}</h1>
|
||||
<div class="my-3">
|
||||
<a class="button is-link mr-3" href="/download-object/tar/{{ object.id }}">Télécharger les données de l'objet (archive TAR)</a>
|
||||
<a class="button is-link" href="/download-object/zip/{{ object.id }}">Télécharger les données de l'objet (archive ZIP)</a>
|
||||
</div>
|
||||
{% if object.acquisitions %}
|
||||
<div class="fixed-grid has-6-cols">
|
||||
<div class="grid">
|
||||
{% for acquisition in object.acquisitions %}
|
||||
<a class="cell" href="/scan-acquisition/{{ acquisition.id }}">
|
||||
<div class="has-text-centered p-3" style="border-radius: 15px; border-width: 1px; border-color: {% if acquisition.validated %}green{% else %}red{% endif %}; border-style: solid;">
|
||||
<div>
|
||||
<img src="/data/objects/{{ object.id }}/{{ acquisition.id }}/{{ leds[0] }}.jpg">
|
||||
</div>
|
||||
<p>
|
||||
{{ acquisition.get_pretty_date() }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if calibration.state == CalibrationState.IsValidated %}
|
||||
<a href="/scan/{{ object.id }}" class="button is-link">Faire un scan</a>
|
||||
{% else %}
|
||||
<button class="button is-link" id="scan">Faire un scan</button>
|
||||
<div id="calibration-modal" class="modal">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="has-text-centered mb-3">Le scanner n'est pas étalonné.</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-centered">
|
||||
<div class="control">
|
||||
<a href="/calibrate/" class="button is-link">Étalonner le scanner</a>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="use-last-calibration-button" href="/use-last-calibration/" class="button is-link">Réutiliser le dernier étalonnage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}{% if calibration.state < CalibrationState.IsValidated %}
|
||||
<script>
|
||||
let modal = document.getElementById('calibration-modal');
|
||||
document.getElementById('scan').addEventListener('click', () => {
|
||||
modal.classList.add('is-active');
|
||||
});
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach((close) => {
|
||||
close.addEventListener('click', () => {
|
||||
modal.classList.remove('is-active');
|
||||
});
|
||||
});
|
||||
document.getElementById('use-last-calibration-button').addEventListener('click', async () => {
|
||||
let resp = await fetch('/api/use-last-calibration');
|
||||
await resp.text();
|
||||
window.location.href = '/scan/{{ object.id }}';
|
||||
});
|
||||
</script>
|
||||
{% endif %}{% endblock %}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="title">Faire une acquisition</h1>
|
||||
<div class="mb-2">
|
||||
<p>Placez l'objet devant le scanner puis appuyez sur le bouton pour lancer l'acquisition.</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
<div class="control">
|
||||
<button {% if acquisition and acquisition.validated %}disabled{% endif %} id="scan-button" class="button is-link">Lancer l'acquisition</button>
|
||||
</div>
|
||||
</div>
|
||||
<article id="error-container" class="message is-danger" style="display: none;">
|
||||
<div id="error-content" class="message-body">
|
||||
</div>
|
||||
</article>
|
||||
<div class="fixed-grid has-8-cols">
|
||||
<div id="grid" class="grid">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<progress id="progress-bar" class="progress is-link" style="display: none;" value="0" max="1000"></progress>
|
||||
<div id="calibrate" {% if not acquisition %}style="display: none;"{% endif %}>
|
||||
<p>Si les données acquises conviennent, appuyez sur le bouton ci-dessous pour valider l'acquisition.</p>
|
||||
{% if acquisition %}
|
||||
{% if acquisition.validated %}
|
||||
<button disabled id="calibrate-button" class="button is-link">Cette acquisition a déjà été validée</button>
|
||||
{% else %}
|
||||
<a href="/validate-acquisition/{{ acquisition.id }}" id="calibrate-button" class="button is-link">Valider l'acquisition</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a id="calibrate-button" class="button is-link">Valider l'acquisition</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
{% block extrajs %}
|
||||
<script>
|
||||
let acquisitionId = {% if acquisition %}{{ acquisition.id }}{% else %}null{% endif %};
|
||||
let scanIndex = 1;
|
||||
let progress = 0;
|
||||
let scanButton = document.getElementById('scan-button');
|
||||
let progressBar = document.getElementById('progress-bar');
|
||||
let grid = document.getElementById('grid');
|
||||
let buttons = [scanButton];
|
||||
let errorContainer = document.getElementById('error-container');
|
||||
let errorContent = document.getElementById('error-content');
|
||||
let calibrateDiv = document.getElementById('calibrate');
|
||||
let calibrateButton = document.getElementById('calibrate-button');
|
||||
|
||||
// If we already have calibration images, we show them right now
|
||||
if (acquisitionId !== null) {
|
||||
let cell, img;
|
||||
{% for led in leds %}
|
||||
cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/{{ led }}.jpg?v=0';
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
scanButton.addEventListener('click', async () => {
|
||||
scanIndex++;
|
||||
progress = 0;
|
||||
progressBar.value = 0;
|
||||
buttons.forEach(x => x.setAttribute('disabled', 'disabled'));
|
||||
scanButton.classList.add('is-loading');
|
||||
progressBar.style.display = "block";
|
||||
grid.innerHTML = '';
|
||||
|
||||
let response;
|
||||
if (acquisitionId === null) {
|
||||
response = await fetch('/api/scan-for-object/{{ object.id }}');
|
||||
} else {
|
||||
response = await fetch('/api/scan-for-acquisition/' + acquisitionId);
|
||||
}
|
||||
|
||||
let reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
|
||||
while (true) {
|
||||
let { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
buttons.forEach(x => x.removeAttribute('disabled'));
|
||||
scanButton.classList.remove('is-loading');
|
||||
calibrateDiv.style.display = "block";
|
||||
break;
|
||||
}
|
||||
|
||||
if (acquisitionId === null) {
|
||||
acquisitionId = parseInt(value, 10);
|
||||
calibrateButton.setAttribute('href', '/validate-acquisition/' + acquisitionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
let [ uuid, ratio ] = value.trim().split(',');
|
||||
progress = Math.ceil(1000 * parseFloat(ratio));
|
||||
let cell = document.createElement('div');
|
||||
cell.classList.add('cell');
|
||||
let img = document.createElement('img');
|
||||
img.classList.add('is-loading');
|
||||
img.src = '/data/objects/{{ object.id }}/' + acquisitionId + '/' + uuid + '.jpg?v=' + scanIndex;
|
||||
cell.appendChild(img);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
});
|
||||
|
||||
function refreshProgressBar() {
|
||||
if (progress !== progressBar.value) {
|
||||
progressBar.value = Math.min(progressBar.value + 5, progress);
|
||||
}
|
||||
requestAnimationFrame(refreshProgressBar);
|
||||
}
|
||||
|
||||
refreshProgressBar();
|
||||
</script>
|
||||
{% endblock extrajs %}
|
||||
|
|
@ -158,6 +158,7 @@ export class Engine {
|
|||
initScene(calibration: Calibration): void {
|
||||
this.center = new THREE.Vector3(0, 0, 10);
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0, 0, 0);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 0.001, 1000);
|
||||
this.camera.position.set(0, 0, -30);
|
||||
|
|
|
|||
Loading…
Reference in New Issue