tracking
This commit is contained in:
@@ -5,11 +5,12 @@ programs=webUI,planification,services
|
||||
process_name=%(program_name)s
|
||||
priority=500
|
||||
directory=/home/rpi4/PlanarianScanner/test_tube_scanner
|
||||
command=
|
||||
/home/rpi4/PlanarianScanner/.venv/bin/daphne
|
||||
-b 0.0.0.0
|
||||
-p 8000
|
||||
home.asgi:application
|
||||
command=/home/rpi4/PlanarianScanner/test_tube_scanner/run-server.sh
|
||||
# /home/rpi4/PlanarianScanner/.venv/bin/daphne
|
||||
# -b 0.0.0.0
|
||||
# -p 8000
|
||||
# home.asgi:application
|
||||
|
||||
user=rpi4
|
||||
group=rpi4
|
||||
stopasgroup=true
|
||||
|
||||
@@ -40,12 +40,14 @@ SECRET_KEY="django-insecure-0)4_w=pjv1ex7=s=c=ii3g@fx_=8fb=hxk€3bpk1)uj(0ph0t)
|
||||
USER=rpi4
|
||||
GROUP=rpi4
|
||||
|
||||
|
||||
DOMAIN_SERVER=scanner.local
|
||||
ALLOWED_HOSTS=127.0.0.1,localhost
|
||||
CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000
|
||||
ALLOWED_HOSTS=127.0.0.1,localhost,scanner.local,192.168.250.230,192.168.1.200
|
||||
CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,http://scanner.local,http://192.168.1.200:8000,http://192.168.250.230:8000
|
||||
|
||||
####
|
||||
# server app
|
||||
LOCAL_IP_SERVER=192.168.1.200
|
||||
SERVER_HOST_IP=0.0.0.0
|
||||
SERVER_HOST_PORT=8000
|
||||
|
||||
|
||||
@@ -370,6 +370,8 @@ GROUP = config('GROUP')
|
||||
SERVER_HOST_PORT = config('SERVER_HOST_PORT', cast=int)
|
||||
SERVER_HOST_IP = config('SERVER_HOST_IP')
|
||||
|
||||
LOCAL_IP_SERVER = config('LOCAL_IP_SERVER')
|
||||
|
||||
# ws
|
||||
SCANNER_WEBSOCKET_ROUTE = 'ws/scanner'
|
||||
REPLAY_WEBSOCKET_ROUTE = 'ws/replay'
|
||||
@@ -387,6 +389,8 @@ DATETIME_FORMAT = '%d-%m-%Y-%m %H:%M:%S'
|
||||
# rpicam 4056x3040 2028x1080 2028x1520
|
||||
#===========================
|
||||
|
||||
GRBL_SIMULATION = True
|
||||
|
||||
EXPORTS_LOCAL_PATH = config("EXPORTS_LOCAL_PATH")
|
||||
EXPORT_REMOTE_PATH = config("EXPORT_REMOTE_PATH")
|
||||
|
||||
|
||||
@@ -101,21 +101,21 @@ class VideoCaptureInterface(abc.ABC):
|
||||
try:
|
||||
if self.use_tracking and cfg:
|
||||
self._tracker = PlanarianTracker(**cfg)
|
||||
logger.info("Tracker de test créé avec conf : %s", cfg)
|
||||
logger.info(f"Tracker de test créé avec conf: {cfg}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating tracker with conf {cfg}: {e}")
|
||||
self._tracker = None
|
||||
|
||||
def on_well_change(self, cfg):
|
||||
def on_well_change(self, cfg, draw_contours=False):
|
||||
"""
|
||||
Appelé par la CNC lors du changement de puits.
|
||||
Réinitialise le fond appris et l'état inter-frame du tracker.
|
||||
Construit les métriques aussi
|
||||
"""
|
||||
if not self.use_tracking:
|
||||
if not self.use_tracking or not cfg:
|
||||
return
|
||||
|
||||
params = self.DEFAULT_TRACKER_CONFIG if not cfg else cfg.to_params_dict()
|
||||
params = cfg.to_params_dict()
|
||||
self._params = ExperimentParams(params)
|
||||
#self._metrics = self._params.build_metrics()
|
||||
|
||||
@@ -128,8 +128,12 @@ class VideoCaptureInterface(abc.ABC):
|
||||
max_planarians = self._params.planarian_count,
|
||||
merge_kernel_size = self._params.merge_kernel_size,
|
||||
min_contour_dist_px = self._params.min_contour_dist_px,
|
||||
draw_contours = draw_contours,
|
||||
)
|
||||
|
||||
def set_draw_contours(self, draw: bool = True):
|
||||
if self._tracker:
|
||||
self._tracker.draw_contours = draw
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Méthodes abstraites — obligatoires dans les sous-classes
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
'''
|
||||
Simulateur GCode pour tester sans CNC physique.
|
||||
|
||||
GRBLController (simulé):
|
||||
Reproduit fidèlement l'API de grbl.py
|
||||
Simule les mouvements (X, Y) avec délai proportionnel au feed rate
|
||||
Le mode absolu est retenu
|
||||
Aucune dépendance à pyserial
|
||||
|
||||
Created on 07 mai 2026
|
||||
|
||||
@author: denis@miraceti.net
|
||||
'''
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
import math
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GRBLController:
|
||||
'''
|
||||
Simulateur du contrôleur GRBL 1.1f (L2544 Laser Engraving Machine).
|
||||
API 100% identique à grbl.py — interchangeable sans modifier le code appelant.
|
||||
Les délais de déplacement sont calculés à partir du feed rate et de la distance.
|
||||
'''
|
||||
X_MAX = 350
|
||||
Y_MAX = 250
|
||||
X_MIN = 0
|
||||
Y_MIN = 0
|
||||
|
||||
# Facteur de compression du temps simulé (1.0 = temps réel, 0.1 = 10x plus rapide)
|
||||
TIME_SCALE = 0.1
|
||||
|
||||
def __init__(self, port='/dev/ttyUSB0', baudrate=115200, timeout=1, send_callback=None, x_max=None, y_max=None):
|
||||
logger.info(f"GRBLController SIMULATOR::init begin {port} device port")
|
||||
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.timeout = timeout
|
||||
|
||||
if x_max is not None:
|
||||
self.X_MAX = x_max
|
||||
if y_max is not None:
|
||||
self.Y_MAX = y_max
|
||||
|
||||
self._state = send_callback
|
||||
if self._state is None:
|
||||
self._state = self._send_msg
|
||||
|
||||
# Position courante simulée
|
||||
self.x, self.y = 0.0, 0.0
|
||||
|
||||
# État interne de la machine simulée
|
||||
self._machine_state = 'Idle' # Idle | Run | Alarm
|
||||
self._connected = False
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Méthodes utilitaires
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def wait_for(self, delay=1.0):
|
||||
# Applique le facteur de compression temporelle
|
||||
threading.Event().wait(delay * self.TIME_SCALE)
|
||||
|
||||
def _send_msg(self, **msg):
|
||||
# Callback par défaut : simple affichage console
|
||||
print(msg)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Simulation de la couche série (pas de port réel)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def clear_buffer(self):
|
||||
# Rien à vider : pas de port série physique
|
||||
logger.debug("SIMULATOR::clear_buffer (no-op)")
|
||||
|
||||
def start_connection(self):
|
||||
'''Simule l'ouverture de la connexion série et l'initialisation GRBL.'''
|
||||
logger.info(f"SIMULATOR::start_connection on {self.port} @ {self.baudrate} baud")
|
||||
self._state(state='serial', msg="Grbl 1.1f ['$' for help]")
|
||||
self._connected = True
|
||||
self._wake_up()
|
||||
self._init_machine()
|
||||
logger.info("SIMULATOR::start_connection started")
|
||||
|
||||
def _init_machine(self):
|
||||
# Envoie les commandes d'initialisation (simulées)
|
||||
self.send("G21") # Unités en mm
|
||||
self.send("G90") # Mode absolu
|
||||
|
||||
def _clamp(self, x, y):
|
||||
self.clear_buffer()
|
||||
x = max(self.X_MIN, min(self.X_MAX, x))
|
||||
y = max(self.Y_MIN, min(self.Y_MAX, y))
|
||||
return x, y
|
||||
|
||||
def _wake_up(self):
|
||||
# Simule l'envoi des octets de réveil et la réponse GRBL
|
||||
logger.debug("SIMULATOR::_wake_up")
|
||||
self.wait_for(1)
|
||||
self._state(state='serial', msg="") # ligne vide typique de GRBL au démarrage
|
||||
self.clear_buffer()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Envoi de commandes
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def send(self, cmd, wait_ok=True, timeout=5):
|
||||
try:
|
||||
return self._send(cmd, wait_ok, timeout)
|
||||
except Exception as e:
|
||||
self._state(state='error', msg=f"Error send {cmd} command: {e}")
|
||||
self.close()
|
||||
self.start_connection()
|
||||
|
||||
def recover(self):
|
||||
self._state(state='recover', msg="Erreur, récupération de GRBL...")
|
||||
self.wait_for(1)
|
||||
self._wake_up()
|
||||
|
||||
def _send(self, cmd, wait_ok=True, timeout=5):
|
||||
'''Simule l'envoi d'une commande GCode et retourne "ok".'''
|
||||
self._state(state='send', msg=f">>> {cmd}")
|
||||
logger.debug(f"SIMULATOR::_send {cmd}")
|
||||
|
||||
# Interprète les commandes de mouvement pour mettre à jour la position interne
|
||||
self._interpret_gcode(cmd)
|
||||
|
||||
if not wait_ok:
|
||||
return None
|
||||
|
||||
# Simule une réponse "ok" immédiate
|
||||
return "ok"
|
||||
|
||||
def _interpret_gcode(self, cmd):
|
||||
'''
|
||||
Analyse le GCode pour mettre à jour x, y et simuler le délai de déplacement.
|
||||
Gère : G0, G1, G53 G1, G92, G21, G90, G91, $X, $H.
|
||||
'''
|
||||
cmd_upper = cmd.strip().upper()
|
||||
|
||||
# --- Commandes sans mouvement ---
|
||||
if cmd_upper in ("G21", "G90", "G91", "$X"):
|
||||
return
|
||||
if cmd_upper == "$H":
|
||||
# Homing : retour à l'origine avec délai simulé
|
||||
self._machine_state = 'Run'
|
||||
self._state(state='send', msg="SIMULATOR: homing...")
|
||||
distance = math.hypot(self.x, self.y)
|
||||
self._simulate_move_delay(distance, feed=3000)
|
||||
self.x, self.y = 0.0, 0.0
|
||||
self._machine_state = 'Idle'
|
||||
return
|
||||
|
||||
# --- Extraction des coordonnées X, Y et du feed F ---
|
||||
tokens = cmd_upper.replace(',', ' ').split()
|
||||
new_x, new_y, feed = self.x, self.y, 1000.0
|
||||
|
||||
for token in tokens:
|
||||
if token.startswith('X'):
|
||||
try:
|
||||
new_x = float(token[1:])
|
||||
except ValueError:
|
||||
pass
|
||||
elif token.startswith('Y'):
|
||||
try:
|
||||
new_y = float(token[1:])
|
||||
except ValueError:
|
||||
pass
|
||||
elif token.startswith('F'):
|
||||
try:
|
||||
feed = float(token[1:])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# --- G92 : redéfinit la position courante sans déplacement ---
|
||||
if 'G92' in tokens:
|
||||
self.x = new_x
|
||||
self.y = new_y
|
||||
logger.debug(f"SIMULATOR: G92 position set to ({self.x:.2f}, {self.y:.2f})")
|
||||
return
|
||||
|
||||
# --- Mouvement effectif (G0, G1, G53 G1, etc.) ---
|
||||
has_move = any(t in tokens for t in ('G0', 'G1', 'G53'))
|
||||
if has_move and (new_x != self.x or new_y != self.y):
|
||||
distance = math.hypot(new_x - self.x, new_y - self.y)
|
||||
self._machine_state = 'Run'
|
||||
self._simulate_move_delay(distance, feed)
|
||||
self.x = new_x
|
||||
self.y = new_y
|
||||
self._machine_state = 'Idle'
|
||||
logger.debug(f"SIMULATOR: moved to ({self.x:.2f}, {self.y:.2f})")
|
||||
|
||||
def _simulate_move_delay(self, distance_mm, feed):
|
||||
'''Simule le temps de déplacement : distance / feed (mm/min) → secondes.'''
|
||||
if feed <= 0:
|
||||
return
|
||||
duration = (distance_mm / feed) * 60.0 # feed est en mm/min
|
||||
self.wait_for(duration)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Status machine
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def get_status(self):
|
||||
'''Retourne un status GRBL simulé au format <State|MPos:x,y,z>.'''
|
||||
status = f"<{self._machine_state}|MPos:{self.x:.3f},{self.y:.3f},0.000|FS:0,0>"
|
||||
logger.debug(f"SIMULATOR::get_status → {status}")
|
||||
return status
|
||||
|
||||
def reset_grbl(self):
|
||||
self.send("$X") # Réinitialise les alarmes
|
||||
self.wait_idle()
|
||||
self.send("$H") # Homing
|
||||
self.wait_idle()
|
||||
|
||||
def _mpos(self, status):
|
||||
if "MPos" in status:
|
||||
mpos = status.split("MPos:")[1].split("|")[0]
|
||||
x, y, *_ = mpos.split(",")
|
||||
self._state(state='Mpos', msg=f"pos >>> ({x}, {y})")
|
||||
return float(x), float(y)
|
||||
return None, None
|
||||
|
||||
def get_mpos(self):
|
||||
return self._mpos(self.get_status())
|
||||
|
||||
def wait_idle(self, timeout=20):
|
||||
'''Attend que la machine soit à l'état Idle (immédiat en simulation).'''
|
||||
start = time.time()
|
||||
while True:
|
||||
if time.time() - start > timeout:
|
||||
raise TimeoutError("Délai d'attente pour Idle dépassé")
|
||||
status = self.get_status()
|
||||
self.x, self.y = self._mpos(status)
|
||||
self._state(xy=True, x=self.x, y=self.y)
|
||||
if status and "Idle" in status:
|
||||
break
|
||||
self.wait_for(0.1)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Commandes de haut niveau (identiques à grbl.py)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def send_command(self, cmd):
|
||||
self.send(cmd)
|
||||
self.wait_idle()
|
||||
|
||||
def move_to(self, x, y, feed=1000):
|
||||
x, y = self._clamp(x, y)
|
||||
cmd = f"G53 G1 X{x:.2f} Y{y:.2f} F{feed}"
|
||||
self.send_command(cmd)
|
||||
|
||||
def move_relative(self, dx=0, dy=0, feed=1000):
|
||||
x, y = self.get_mpos() # Position actuelle
|
||||
self.move_to(x + dx, y + dy, feed=feed)
|
||||
|
||||
def move_relative__(self, dx=0, dy=0, feed=1000):
|
||||
self.send("G91") # Mode relatif
|
||||
cmd = f"G0 X{dx} Y{dy} F{feed}"
|
||||
self.send(cmd)
|
||||
self.send("G90") # Retour en mode absolu
|
||||
self.wait_idle()
|
||||
|
||||
def go_origin(self, feed=1000):
|
||||
self.move_to(0, 0, feed=feed)
|
||||
self.wait_for(2.0)
|
||||
|
||||
def set_position(self, x, y):
|
||||
x, y = self._clamp(x, y)
|
||||
cmd = f"G92 X{x:.2f} Y{y:.2f}"
|
||||
self.send(cmd)
|
||||
self.wait_for(2.0)
|
||||
|
||||
def move_up(self, step=10, feed=1000):
|
||||
self.move_relative(dy=step, feed=feed)
|
||||
|
||||
def move_down(self, step=10, feed=1000):
|
||||
self.move_relative(dy=-step, feed=feed)
|
||||
|
||||
def move_left(self, step=10, feed=1000):
|
||||
self.move_relative(dx=-step, feed=feed)
|
||||
|
||||
def move_right(self, step=10, feed=1000):
|
||||
self.move_relative(dx=step, feed=feed)
|
||||
|
||||
def close(self):
|
||||
# Simule la fermeture du port série
|
||||
self._connected = False
|
||||
logger.info("SIMULATOR::close — connexion simulée fermée")
|
||||
@@ -172,6 +172,7 @@ class PlanarianTracker:
|
||||
max_planarians: int = 1,
|
||||
merge_kernel_size: int = 15,
|
||||
min_contour_dist_px:int = 40,
|
||||
draw_contours: bool = True,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -189,6 +190,7 @@ class PlanarianTracker:
|
||||
self.min_area_px = min_area_px
|
||||
self.max_area_ratio = max_area_ratio
|
||||
self.max_planarians = max(1, min(max_planarians, MAX_PLANARIANS))
|
||||
self.draw_contours = draw_contours
|
||||
|
||||
# Un état inter-frame par slot individu
|
||||
self._states = [PlanarianState(i) for i in range(self.max_planarians)]
|
||||
@@ -368,6 +370,9 @@ class PlanarianTracker:
|
||||
# Mise à jour de l'état
|
||||
state.update(cx, cy, ts)
|
||||
|
||||
|
||||
if self.draw_contours:
|
||||
|
||||
# Annotation visuelle
|
||||
color = INDIVIDUAL_COLORS[slot_idx % len(INDIVIDUAL_COLORS)]
|
||||
cv2.drawContours(frame_out, [contour], -1, color, 2)
|
||||
|
||||
@@ -21,7 +21,7 @@ class ExperimentConfigAdmin(admin.ModelAdmin):
|
||||
(_("Identification"), {
|
||||
"fields": ("identifier", "experiment", "well", "description"),
|
||||
}),
|
||||
(_("Calibration optique"), {
|
||||
(_("Calibration optique: générée lors de la calibration"), {
|
||||
"fields": ("px_per_mm", "fps", "well_radius_mm"),
|
||||
"classes": ("collapse",),
|
||||
}),
|
||||
|
||||
@@ -77,13 +77,13 @@ class ExperimentConfig(models.Model):
|
||||
|
||||
merge_kernel_size = models.PositiveIntegerField(
|
||||
_("Taille du kernel"),
|
||||
help_text=_("taille du kernel elliptique de fusion des fragments (px)"),
|
||||
help_text=_("taille du kernel elliptique de fusion des fragments (px). Augmenter si fragments résiduels"),
|
||||
default=15
|
||||
)
|
||||
|
||||
min_contour_dist_px = models.PositiveIntegerField(
|
||||
_("Distance <contour>"),
|
||||
help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px."),
|
||||
help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px. Augmenter si IDs multiples persistent"),
|
||||
default=40
|
||||
)
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
Section 5 : Comportements (accordéon W3.CSS)
|
||||
Section 5 : Comportements
|
||||
============================================================ -->
|
||||
<div class="w3-card-4 w3-round-large w3-margin-bottom w3-border w3-round w3-round-large">
|
||||
<header class="w3-container w3-teal w3-round w3-round-top-large">
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "==== Starting server development in debug mode port 8000 ..."
|
||||
echo "===="
|
||||
../.venv/bin/python manage.py runserver 0.0.0.0:8000
|
||||
@@ -28,6 +28,7 @@ class DefaultConfig:
|
||||
calibration_default_step: float = 1.0
|
||||
calibration_default_duration: float = 3.0
|
||||
tracking: bool = False
|
||||
tube_axis: str = 'vertical'
|
||||
min_area_px: int = 20
|
||||
max_area_ratio: float = 0.10
|
||||
max_planarians: int = 1
|
||||
|
||||
@@ -38,6 +38,11 @@ CAPTURE_TYPE = [
|
||||
('file', _("mp4")),
|
||||
]
|
||||
|
||||
TUBE_AXIS_TYPE = [
|
||||
('vertical', _("Vertical")),
|
||||
('horizontal', _("Horizontal")),
|
||||
]
|
||||
|
||||
class Configuration(models.Model):
|
||||
name = models.CharField(_("Nom de la Configuration"), help_text=_("Nom de la configuration"), max_length=100, null=True, blank=False, default=_("Configuration par défaut"))
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
@@ -66,11 +71,12 @@ class Configuration(models.Model):
|
||||
calibration_default_duration = models.FloatField(_("Duruée calibration"), help_text=_("Durée de pose entre chaque puits en s"), default=3.0)
|
||||
# tracking
|
||||
tracking = models.BooleanField(_("Suivi"), help_text=_("Suivi et analyse des planaires"), default=False)
|
||||
tube_axis = models.CharField(_("Axe du puit"), help_text=_("Axe du tube"), default='vertical', max_length=16, choices=TUBE_AXIS_TYPE, null=True, blank=False)
|
||||
min_area_px = models.PositiveIntegerField(_("Surface minimale"), help_text=_("surface minimale d'un contour pour être considéré valide (px²)"), default=20)
|
||||
max_area_ratio = models.FloatField(_("surface maximale "), help_text=_("surface maximale d'un contour en fraction de la frame (défaut 10%)"), default=0.10)
|
||||
max_planarians = models.PositiveIntegerField(_("Max planaire"), help_text=_("nombre maximum de planaires à suivre simultanément (1-10)"), default=1)
|
||||
merge_kernel_size = models.PositiveIntegerField(_("Taille du kernel"), help_text=_("taille du kernel elliptique de fusion des fragments (px)"), default=15)
|
||||
min_contour_dist_px = models.PositiveIntegerField(_("Distance <contour>"), help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px."), default=40)
|
||||
merge_kernel_size = models.PositiveIntegerField(_("Taille du kernel"), help_text=_("taille du kernel elliptique de fusion des fragments (px). Augmenter si fragments résiduels"), default=15)
|
||||
min_contour_dist_px = models.PositiveIntegerField(_("Distance <contour>"), help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px. Augmenter si IDs multiples persistent"), default=40)
|
||||
#
|
||||
active = models.BooleanField(_("Actif"), default=False)
|
||||
|
||||
@@ -115,7 +121,7 @@ class MultiWell(models.Model):
|
||||
row_order = models.CharField(_("Ordre ligne"), help_text=_('Ordre ligne de puit. Lecture en serpentin dans le sens des +- X'), max_length=16, null=True, blank=False, default="D,C,B,A")
|
||||
# Balayage
|
||||
order = models.PositiveSmallIntegerField(_("Ordre"), help_text=_('Ordre de lecture du multi-puit'), blank=False, default=0)
|
||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée du film en secondes'), blank=False, default=120)
|
||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée du film en secondes por la calibration'), blank=False, default=10)
|
||||
xbase = models.FloatField(_("Origine X"), help_text=_('Base origine X en mm'), blank=False, default=50.0)
|
||||
ybase = models.FloatField(_("Origine Y"), help_text=_('Base origine Y en mm'), blank=False, default=50.0)
|
||||
|
||||
@@ -231,6 +237,7 @@ class Experiment(models.Model):
|
||||
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée de la prise de vue en secondes'), blank=False, default=120)
|
||||
created = models.DateTimeField(_("Date de création"), default=timezone.now)
|
||||
started = models.DateTimeField (_("Date de début"), null=True, blank=True)
|
||||
finished = models.DateTimeField (_("Date de fin"), null=True, blank=True)
|
||||
|
||||
@@ -149,14 +149,15 @@ class MultiWellManager:
|
||||
|
||||
#def _grid_scanning_capture(self, uuid, duration):
|
||||
def _grid_scanning_capture(self, experiment, well_position, simulate=False):
|
||||
try:
|
||||
well = well_position.well
|
||||
multiwell = experiment.multiwell
|
||||
if self.process.use_tracking:
|
||||
logger.info("# Paramètres d'une expérience PlanarianScanner")
|
||||
|
||||
cfg = ExperimentConfig.objects.get(experiment_id=experiment.id, well_id=well.id)
|
||||
cfg = ExperimentConfig.objects.filter(experiment_id=experiment.id, well_id=well.id).first()
|
||||
if not cfg:
|
||||
raise Exception(f"Configuration d'expérience introuvable pour {experiment} / {well}")
|
||||
# reset PlanarianTracker => on_well_change
|
||||
self.process.cam.on_well_change(cfg)
|
||||
self.process.cam.on_well_change(cfg, draw_contours=False)
|
||||
|
||||
uuid = f'{self.process.data.session}-{multiwell.position}-{well.name}'
|
||||
## start recording
|
||||
@@ -173,18 +174,30 @@ class MultiWellManager:
|
||||
self.process.data.record = False
|
||||
self.process.data.uuid = None
|
||||
|
||||
msg = f"{uuid}: capture done"
|
||||
msg = f"{uuid}: capture done..."
|
||||
except Exception as e:
|
||||
msg = f"error during capture - {e}"
|
||||
logger.error(msg)
|
||||
|
||||
logger.info(msg)
|
||||
self.process._send(scan_state=msg)
|
||||
|
||||
|
||||
def _capture_file_simulation(self, name):
|
||||
vf = settings.MEDIA_ROOT / 'simulation' / f'{name}.mp4'
|
||||
if vf.exists():
|
||||
self.process.cam._video_file = str(vf)
|
||||
self.process.cam._error_occured = True
|
||||
logger.info(f"Simulating capture with file {vf}")
|
||||
|
||||
|
||||
def _grid_scanning(self, experiment, xnext=0, ynext=0, simulate=False):
|
||||
try:
|
||||
multiwell = experiment.multiwell
|
||||
wells = models.WellPosition.objects.filter(multiwell_id=multiwell.id).order_by('order').all()
|
||||
cam = self.process.cam
|
||||
cam._aligner.set_tube_diameter(multiwell.diameter)
|
||||
|
||||
self.stop_playing = Event()
|
||||
for wl in wells:
|
||||
if self.stop_playing.is_set():
|
||||
break
|
||||
@@ -192,23 +205,27 @@ class MultiWellManager:
|
||||
|
||||
## change file
|
||||
if self.process.conf.capture_type == 'file':
|
||||
vf = settings.MEDIA_ROOT / 'simulation' / f'{wl.well.name}.mp4'
|
||||
if vf.exists():
|
||||
self.process.cam._video_file = str(vf)
|
||||
self.process.cam._error_occured = True
|
||||
|
||||
logger.info(f"Simulating capture with file {vf}")
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
|
||||
self._grid_scanning_capture(experiment, wl, simulate=simulate)
|
||||
msg =f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})"
|
||||
logger.info(msg)
|
||||
self.process._send(state='scan_finished', msg=msg)
|
||||
|
||||
except Exception as e:
|
||||
msg = f"Error during grid scanning - {e}"
|
||||
logger.error(msg)
|
||||
self.process._send(state='error', msg=msg)
|
||||
|
||||
logger.info(f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})")
|
||||
finally:
|
||||
self.cnc_controller.move_to(xnext, ynext, feed=multiwell.feed*2)
|
||||
|
||||
|
||||
def _start_scanning(self, session, experiments, simulate=False):
|
||||
try:
|
||||
self.process.cam._aligner.debug = False
|
||||
self.stop_playing.clear()
|
||||
|
||||
xynext = []
|
||||
for obs in experiments:
|
||||
xynext.append((obs.multiwell.xbase, obs.multiwell.ybase))
|
||||
@@ -240,6 +257,8 @@ class MultiWellManager:
|
||||
msg = f"Session {session.name} terminée à {session.finished} après {session.finished - started} secondes."
|
||||
logger.info(msg)
|
||||
self.process._send(scan_state=msg)
|
||||
except Exception as e:
|
||||
logger.error("Error during scanning process", e)
|
||||
finally:
|
||||
self.scan_thread = None
|
||||
|
||||
@@ -265,20 +284,28 @@ class MultiWellManager:
|
||||
|
||||
def previous_well(self):
|
||||
wl = self.well_iterator.previous()
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
return {"state": "previous", "msg": f">>> ({wl.x}, {wl.y})"}
|
||||
return {"state": "previous", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"}
|
||||
|
||||
|
||||
def next_well(self):
|
||||
wl = self.well_iterator.next()
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
return {"state": "next", "msg": f">>> ({wl.x}, {wl.y})"}
|
||||
return {"state": "next", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"}
|
||||
|
||||
|
||||
def goto_well(self, numwell):
|
||||
wl = self.well_iterator.seek(numwell)
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
return {"state": "goto", "msg": f">>> ({wl.x}, {wl.y})"}
|
||||
return {"state": "goto", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"}
|
||||
|
||||
|
||||
def set_well_position(self):
|
||||
@@ -293,7 +320,7 @@ class MultiWellManager:
|
||||
|
||||
|
||||
def _scanning_test(self, auto=False):
|
||||
self.stop_playing = Event()
|
||||
self.stop_playing.clear()
|
||||
cam = self.process.cam
|
||||
cam._aligner.set_tube_diameter(self.multiwell.diameter)
|
||||
duration = self.duration if not auto else settings.CALIBRATION_AUTO_DURATION
|
||||
@@ -356,7 +383,6 @@ class MultiWellManager:
|
||||
self.test_thread = Thread(target=self._scanning_test, args=(auto, ), daemon=True)
|
||||
self.test_thread.start()
|
||||
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
@@ -421,14 +447,12 @@ class MultiWellManager:
|
||||
def dy(self, value):
|
||||
self._dy = value
|
||||
|
||||
|
||||
def get_well_order(self):
|
||||
wl = self.well_iterator.get_current()
|
||||
if wl:
|
||||
return wl.order
|
||||
return None
|
||||
|
||||
|
||||
def set_position(self):
|
||||
x, y = self.cnc_controller.get_mpos()
|
||||
self.cnc_controller.wait_for(2.0)
|
||||
@@ -439,7 +463,6 @@ class MultiWellManager:
|
||||
wl.px_per_mm = self.px_per_mm
|
||||
wl.save()
|
||||
|
||||
|
||||
def calib_toggle_debug(self):
|
||||
""" Active / désactive le mode debug sur le stream."""
|
||||
aligner = self.process.cam._aligner
|
||||
|
||||
@@ -21,12 +21,19 @@ from celery.exceptions import Ignore
|
||||
from celery.utils.log import get_task_logger
|
||||
from redis import Redis
|
||||
from dataclasses import dataclass
|
||||
from modules import reductstore, grbl, utils, planarian_metrics
|
||||
from modules import reductstore, utils, planarian_metrics
|
||||
|
||||
## camera devices
|
||||
from modules.circular_crop import CircularCrop, CropStrategy
|
||||
from .multiwell import MultiWellManager
|
||||
from .constants import ScannerConstants
|
||||
|
||||
# CNC
|
||||
if not settings.GRBL_SIMULATION:
|
||||
from modules.grbl import GRBLController # @UnusedImport
|
||||
else:
|
||||
from modules.grbl_simulator import GRBLController # @Reimport
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@@ -223,7 +230,7 @@ class ScannerProcess(Task):
|
||||
self.cam._active_median = False
|
||||
self.cam.set_circular_crop(None)
|
||||
|
||||
self.grbl = grbl.GRBLController(
|
||||
self.grbl = GRBLController(
|
||||
send_callback=self._display,
|
||||
x_max=self.conf.grbl_xmax,
|
||||
y_max=self.conf.grbl_ymax
|
||||
@@ -328,6 +335,8 @@ class ScannerProcess(Task):
|
||||
self._send(state='serial', msg=f"Connected {self.grbl.port}")
|
||||
|
||||
self.grbl.go_origin(feed=feed)
|
||||
if self.conf.capture_type == 'file':
|
||||
self.manager._capture_file_simulation('zero')
|
||||
self.grbl.wait_for(2.0)
|
||||
|
||||
|
||||
@@ -363,6 +372,7 @@ class ScannerProcess(Task):
|
||||
self.cam.set_circular_crop(self.crop)
|
||||
self.cam._active_median = False
|
||||
self.grbl.go_origin(feed=self.manager.feed)
|
||||
self.cam.set_draw_contours(False)
|
||||
|
||||
elif topic == 'scan' or topic == 'simulate':
|
||||
logger.info(f"==== Scan {cmd}")
|
||||
@@ -371,7 +381,6 @@ class ScannerProcess(Task):
|
||||
if sid == "0":
|
||||
self._send(state='error', msg=str(_('La session est nulle!...')))
|
||||
else:
|
||||
|
||||
try:
|
||||
self.cam._active_median = False
|
||||
simulate = (topic=='simulate')
|
||||
@@ -398,6 +407,7 @@ class ScannerProcess(Task):
|
||||
self.manager.set_multiwell(position)
|
||||
self.cam.set_circular_crop(None)
|
||||
self.cam._active_median = False
|
||||
self.cam.set_draw_contours(False)
|
||||
buttons = self.manager.multiwell_buttons()
|
||||
|
||||
elif topic == 'up':
|
||||
@@ -444,10 +454,15 @@ class ScannerProcess(Task):
|
||||
elif topic == 'goto_0':
|
||||
self.grbl.go_origin(feed=self.manager.feed)
|
||||
self.manager.well_iterator.reset()
|
||||
self.manager.well_iterator.seek(0)
|
||||
if self.conf.capture_type == 'file':
|
||||
self.manager._capture_file_simulation('zero')
|
||||
|
||||
elif topic == 'goto_xy':
|
||||
self.grbl.move_to(self.manager.xbase, self.manager.ybase, feed=self.manager.feed)
|
||||
self.manager.well_iterator.seek(0)
|
||||
wl = self.manager.well_iterator.seek(0)
|
||||
if self.conf.capture_type == 'file':
|
||||
self.manager._capture_file_simulation(wl.well.name)
|
||||
|
||||
elif topic == 'xy_base':
|
||||
self.manager.set_position()
|
||||
@@ -495,9 +510,10 @@ class ScannerProcess(Task):
|
||||
msg = self.manager.set_well_position()
|
||||
self._send(**msg)
|
||||
|
||||
elif topic == 'track':
|
||||
self.cam.use_tracking = value=="1"
|
||||
self._send(state=topic, msg=f"Tracking: {self.cam.use_tracking}")
|
||||
elif topic == 'draw':
|
||||
draw = (value=="1")
|
||||
self.cam.set_draw_contours(draw)
|
||||
self._send(state=topic, msg=f"Tracking contour: {draw}")
|
||||
|
||||
elif topic in ['min_area_px', 'max_area_ratio', 'max_planarians', 'merge_kernel_size', 'min_contour_dist_px']:
|
||||
value = int(value) if topic in ['min_area_px', 'max_planarians', 'merge_kernel_size', 'min_contour_dist_px'] else float(value)
|
||||
|
||||
@@ -43,12 +43,14 @@ class ScannerManager {
|
||||
this.crop_radius = options.crop_radius;
|
||||
this.calib_auto = options.calib_auto;
|
||||
|
||||
try {
|
||||
this.min_area_px = options.min_area_px;
|
||||
this.max_area_ratio = options.max_area_ratio;
|
||||
this.max_planarians = options.max_planarians;
|
||||
this.merge_kernel_size = options.merge_kernel_size;
|
||||
this.min_contour_dist_px = options.min_contour_dist_px;
|
||||
this.track = options.track;
|
||||
this.draw = options.draw;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
init_controls() {
|
||||
@@ -79,12 +81,14 @@ class ScannerManager {
|
||||
this.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); });
|
||||
this.halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
||||
|
||||
try {
|
||||
this.min_area_px.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "min_area_px", value: e.target.value }); });
|
||||
this.max_area_ratio.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "max_area_ratio", value: e.target.value }); });
|
||||
this.max_planarians.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "max_planarians", value: e.target.value}); });
|
||||
this.merge_kernel_size.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "merge_kernel_size", value: e.target.value}); });
|
||||
this.min_contour_dist_px.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "min_contour_dist_px", value: e.target.value }); });
|
||||
this.track.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "track", value: e.target.value }); });
|
||||
this.draw.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "draw", value: e.target.value }); });
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
@@ -100,16 +104,6 @@ class ScannerManager {
|
||||
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||
|
||||
/*
|
||||
if (payload.detected && use_tracking) {
|
||||
this.cx.textContent = payload.cx; this.cy.textContent = payload.cy;
|
||||
this.speed_px_s.textContent = payload.speed_px_s;
|
||||
this.axial_speed.textContent = payload.axial_speed;
|
||||
this.axial_pos.textContent = payload.axial_pos;
|
||||
this.area_px.textContent = payload.area_px;
|
||||
this.frame_count.textContent = payload.count;
|
||||
}*/
|
||||
|
||||
if (payload.buttons) { this.well_btn.innerHTML = payload.buttons; }
|
||||
if (payload.current >= 0) {
|
||||
document.querySelectorAll('button.w3-button.well').forEach(btn => {
|
||||
@@ -117,7 +111,6 @@ class ScannerManager {
|
||||
btn.classList.remove('w3-green');
|
||||
});
|
||||
}
|
||||
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,43 @@
|
||||
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-right w3-xlarge w3-text-green" onclick="goFullscreen()" title="{% trans 'Ecran plein' %}">
|
||||
<i class="fa-solid fa-maximize"></i>
|
||||
</a>
|
||||
{% if request.user.is_superuser %}
|
||||
<div class="w3-dropdown-hover w3-right">
|
||||
<button class="w3-button w3-dark-plus">☰ <span>{% trans "Administrations" %}</span></button>
|
||||
<div class="w3-dropdown-content w3-bar-block w3-border w3-dark-low" style="right:.25em;">
|
||||
<hr class="w3-bar-item divider w3-grey">
|
||||
|
||||
<a href="{% url 'scanner:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity" title="{% trans "Accès django" %}">
|
||||
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Base de données" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:supervisor' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank" title="{% trans "Accès système" %}">
|
||||
<i class="fa-solid fa-gears w3-text-purple"></i> {% trans "Supervision des services" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'scanner:reductstore' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank" title="{% trans "Accès système" %}">
|
||||
<img class="w3-no-padding" src="/static/img/reductstore.png" style="width: 24px"> {% trans "Base de données Reductstore" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:adminer' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank" title="{% trans "Accès système" %}">
|
||||
<img class="w3-no-padding" src="/static/img/adminer.png" style="width: 28px"> {% trans "Base de données mariadb" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'scanner:logs_scheduler' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-orange"></i> {% trans "Logs des planifications" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des services" %}
|
||||
</a>
|
||||
|
||||
<hr class="w3-bar-item divider w3-grey">
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button class="w3-bar-item w3-btn w3-hover-opacity" type="submit">
|
||||
<i class="fa-solid fa-right-from-bracket w3-text-red"></i> {% trans 'Déconnexion' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block sidebar %}
|
||||
@@ -63,45 +100,22 @@
|
||||
{% block sidebar_list %}
|
||||
<div class="w3-dark-light w3-padding-small">
|
||||
{% block sidebar_command %}{% endblock %}
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<div>
|
||||
<div class="w3-dark-light w3-margin-top-6">
|
||||
<a href="#" class="w3-btn w3-padding-small" onclick="toggleDisplay(sId('_admin'))"><span class="w3-text-orange w3-xlarge">⛶</span>
|
||||
{% trans "Administration" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="_admin" style="display:none">
|
||||
<a href="{% url 'scanner:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
|
||||
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Administration base de données" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:reductstore' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<img class="w3-no-padding" src="/static/img/reductstore.png" style="width: 24px"> {% trans "Base de données Reductstore" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:adminer' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<img class="w3-no-padding" src="/static/img/adminer.png" style="width: 28px"> {% trans "Base de données mariadb" %}
|
||||
</a>
|
||||
<!--a href="{% url 'scanner:portainer' %}" class="w3-bar-item w3-btn w3-hover-opacity" w3-margin-left target="_blank">
|
||||
<img class="w3-no-padding" src="/static/img/portainer.png" style="width: 28px"> {% trans "Portainer" %}
|
||||
</a-->
|
||||
<a href="{% url 'scanner:supervisor' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<i class="fa-solid fa-gears w3-text-purple"></i> {% trans "Supervisor" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:logs_scheduler' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-orange"></i> {% trans "Logs des planifications" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des services" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block sidebar_smenu %}
|
||||
{% if request.user.is_superuser and request.user.is_staff %}
|
||||
<a href="{% url 'scanner:calibration' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-bottom">
|
||||
{% if request.user.is_superuser or request.user.is_staff %}
|
||||
<a href="{% url 'scanner:session_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-gear w3-text-purple w3-xlarge"></i> {% trans "Création de sessions d'expériences" %}
|
||||
</a>
|
||||
<hr class="w3-bar-item divider w3-grey">
|
||||
<a href="{% url 'scanner:experimentconfig_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-gears w3-text-pink w3-xlarge"></i> {% trans "Configuration des expériences" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:experiment_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-gear w3-text-pink w3-xlarge"></i> {% trans "Gestions des expériences" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:calibration' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-wrench w3-text-red w3-xlarge"></i> {% trans "Calibration" %}
|
||||
</a>
|
||||
<hr class="w3-bar-item divider w3-grey w3-margin-bottom">
|
||||
{% endif %}
|
||||
{% if conf.tracking %}
|
||||
<a href="{% url 'planarian:experiment-list' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
|
||||
@@ -149,10 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if conf.tracking %}
|
||||
<div class="w3-row w3-row-padding w3-light-grey w3-margin-1 w3-round w3-round-large w3-margin-top">
|
||||
<div class="w3-col"><input id="_track_it" type="checkbox" value="0" onclick="if (this.checked){this.value='1';}else{ this.value='0';}">
|
||||
{% trans 'Configuration du tracking' %}
|
||||
</div>
|
||||
<div class="w3-row w3-row-padding w3-light-grey w3-padding-small w3-margin-1 w3-round w3-round-large w3-margin-top w3-left-align w3-paddin-top">
|
||||
<div class="w3-half">
|
||||
<input id="_min_area_px" type="number" min="5" max="100" step="1" class="w3-round" value="{{ conf.min_area_px }}"
|
||||
title="{% trans 'surface minimale de contour pour être considéré valide (px carré)' %}"><br>{% trans 'Min contour' %}
|
||||
@@ -173,6 +170,10 @@
|
||||
<input id="_max_planarians" type="number" min="1" max="10" step="1" class="w3-round" value="{{ conf.max_planarians }}"
|
||||
title="{% trans 'Nombre maximum de planaires à suivre simultanément (1-10)' %}"><br>{% trans 'Planaires' %}
|
||||
</div>
|
||||
<div class="w3-half">
|
||||
<input id="_draw" type="checkbox" value="0" onclick="if (this.checked){this.value='1';}else{ this.value='0';}" title="{% trans 'Dessiner le contour' %}">
|
||||
<span class="w3-nbsp">{% trans 'Contour' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="w3-row w3-row-padding">
|
||||
@@ -260,7 +261,7 @@
|
||||
max_planarians : sId("_max_planarians"),
|
||||
merge_kernel_size : sId("_merge_kernel_size"),
|
||||
min_contour_dist_px : sId("_min_contour_dist_px"),
|
||||
track : sId("_track_it")
|
||||
draw : sId("_draw")
|
||||
};
|
||||
</script>
|
||||
<script src="/static/scanner/js/calibration.js"></script>
|
||||
|
||||
@@ -14,6 +14,11 @@ urlpatterns = [
|
||||
path('logs/worker', views.supervisor_worker, name='logs_worker'),
|
||||
path('logs/scheduler', views.supervisor_scheduler, name='logs_scheduler'),
|
||||
|
||||
path('session/view', views.admin_session_view, name='session_view'),
|
||||
path('experiment/view', views.admin_experiment_view, name='experiment_view'),
|
||||
path('experiment/config/view', views.admin_experimentconfig_view, name='experimentconfig_view'),
|
||||
path('periodictask/view', views.admin_periodictask_view, name='periodictask_view'),
|
||||
|
||||
path('scanning/', views.scanning_view, name='scanning'),
|
||||
path('images/', views.images_view, name='images'),
|
||||
path('replay/', views.replay_view, name='replay'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
from asgiref.sync import async_to_sync
|
||||
import base64, json
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render #, redirect
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
@@ -12,7 +12,7 @@ from reduct.time import unix_timestamp_to_iso
|
||||
from modules.system_stats import get_cached_stats, start_background_updater
|
||||
from modules import reductstore
|
||||
|
||||
from .tasks import download_video, export_all_images, export_all_videos, supervisor_restart_service
|
||||
from .tasks import download_video, export_all_images, export_all_videos
|
||||
from .process import CameraRecordManager, cameraDB
|
||||
from . import models
|
||||
from .constants import ScannerConstants
|
||||
@@ -39,6 +39,8 @@ def global_context(request, **ctx):
|
||||
app_title=settings.APP_TITLE,
|
||||
app_sub_title=settings.APP_SUB_TITLE,
|
||||
domain_server=settings.DOMAIN_SERVER,
|
||||
local_ip_server=settings.LOCAL_IP_SERVER,
|
||||
host_port=settings.SERVER_HOST_PORT,
|
||||
conf=conf,
|
||||
default_position = default_multiwell.position or 'HD',
|
||||
export_destination=settings.EXPORT_DESTINATIONS,
|
||||
@@ -49,29 +51,45 @@ def global_context(request, **ctx):
|
||||
def admin_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/'))
|
||||
|
||||
@login_required
|
||||
def admin_session_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/session/'))
|
||||
|
||||
@login_required
|
||||
def admin_experiment_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/experiment/'))
|
||||
|
||||
@login_required
|
||||
def admin_experimentconfig_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/planarian/experimentconfig/'))
|
||||
|
||||
@login_required
|
||||
def admin_periodictask_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/django_celery_beat/periodictask/'))
|
||||
|
||||
@login_required
|
||||
def reductstore_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:8383/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:8383/'))
|
||||
|
||||
@login_required
|
||||
def adminer_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}/adminer/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}/adminer/'))
|
||||
|
||||
@login_required
|
||||
def portainer_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9000/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9000/'))
|
||||
|
||||
@login_required
|
||||
def supervisor_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9001/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/'))
|
||||
|
||||
@login_required
|
||||
def supervisor_worker(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9001/logtail/test_tube:services'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:services'))
|
||||
|
||||
@login_required
|
||||
def supervisor_scheduler(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9001/logtail/test_tube:planification'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:planification'))
|
||||
|
||||
## Mainboard
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user