planarian
This commit is contained in:
@@ -2,12 +2,13 @@ from django.contrib import admin
|
||||
from django.db.models import Q
|
||||
from . import models
|
||||
|
||||
|
||||
class WellAdmin(admin.ModelAdmin):
|
||||
model = models.Well
|
||||
list_display = ('name', 'author',)
|
||||
|
||||
class ConfigurationAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'author', 'use_rpicam', 'video_width_capture', 'video_height_capture', 'video_frame_rate', 'active',)
|
||||
list_display = ('name', 'author', 'capture_type', 'video_width_capture', 'video_height_capture', 'video_frame_rate', 'active',)
|
||||
|
||||
class MultiWellAdmin(admin.ModelAdmin):
|
||||
list_filter = ('author', )
|
||||
@@ -15,33 +16,33 @@ class MultiWellAdmin(admin.ModelAdmin):
|
||||
|
||||
class WellPositionAdmin(admin.ModelAdmin):
|
||||
list_filter = ('author', 'multiwell')
|
||||
list_display = ('multiwell__position', 'well__name', 'order', 'x', 'y', 'author',)
|
||||
list_display = ('multiwell__position', 'well__name', 'order', 'x', 'y', 'px_per_mm', 'author',)
|
||||
|
||||
|
||||
class ObservationMultiWellDetailInline(admin.TabularInline):
|
||||
model = models.ObservationMultiWellDetail
|
||||
extra = 0
|
||||
#class ExperimentConfigInline(admin.TabularInline):
|
||||
# model = models.ExperimentConfig
|
||||
# extra = 0
|
||||
|
||||
class ObservationAdmin(admin.ModelAdmin):
|
||||
inlines = (ObservationMultiWellDetailInline,)
|
||||
list_filter = ('sessionobservation__session', 'author', )
|
||||
class ExperimentAdmin(admin.ModelAdmin):
|
||||
#inlines = (ExperimenConfigInline,)
|
||||
list_filter = ('session_experiments__session', 'author', )
|
||||
list_display = ('title', 'author', 'multiwell', 'created', 'started', 'finished')
|
||||
readonly_fields = ('created', 'started', 'finished', )
|
||||
|
||||
class SessionObservationInlineAdmin(admin.TabularInline):
|
||||
model = models.SessionObservation
|
||||
class SessionExperimentInlineAdmin(admin.TabularInline):
|
||||
model = models.SessionExperiment
|
||||
fk_name = 'session'
|
||||
extra = 0
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "observation":
|
||||
if db_field.name == "experiment":
|
||||
obj_id = request.resolver_match.kwargs.get("object_id")
|
||||
|
||||
qs = models.Observation.objects.filter(sessionobservation__isnull=True)
|
||||
qs = models.Experiment.objects.filter(session_experiments__isnull=True)
|
||||
if obj_id:
|
||||
qs = models.Observation.objects.filter(
|
||||
Q(sessionobservation__isnull=True) |
|
||||
Q(sessionobservation__session_id=obj_id)
|
||||
qs = models.Experiment.objects.filter(
|
||||
Q(session_experiments__isnull=True) |
|
||||
Q(session_experiments__session_id=obj_id)
|
||||
)
|
||||
kwargs["queryset"] = qs.distinct()
|
||||
|
||||
@@ -49,7 +50,7 @@ class SessionObservationInlineAdmin(admin.TabularInline):
|
||||
|
||||
class SessionAdmin(admin.ModelAdmin):
|
||||
list_filter = ('author',)
|
||||
inlines = (SessionObservationInlineAdmin, )
|
||||
inlines = (SessionExperimentInlineAdmin, )
|
||||
list_display = ('name', 'author', 'created', 'finished', 'active', 'expected_export', 'expected_scanning', )
|
||||
readonly_fields = (
|
||||
'created',
|
||||
@@ -65,6 +66,7 @@ class SessionAdmin(admin.ModelAdmin):
|
||||
admin.site.register(models.Configuration, ConfigurationAdmin)
|
||||
admin.site.register(models.Well, WellAdmin)
|
||||
admin.site.register(models.MultiWell, MultiWellAdmin)
|
||||
admin.site.register(models.WellPostion, WellPositionAdmin)
|
||||
admin.site.register(models.Observation, ObservationAdmin)
|
||||
admin.site.register(models.WellPosition, WellPositionAdmin)
|
||||
admin.site.register(models.Experiment, ExperimentAdmin)
|
||||
admin.site.register(models.Session, SessionAdmin)
|
||||
|
||||
|
||||
@@ -512,93 +512,5 @@ def _resize_frame(
|
||||
|
||||
return frame
|
||||
|
||||
# tasks/export_tasks.py
|
||||
|
||||
from celery import shared_task, group
|
||||
from django.utils import timezone
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def run_session_exports(self, session_id: str):
|
||||
"""
|
||||
Point d'entrée déclenché par django_celery_beat.
|
||||
Lance en parallèle l'export images et l'export vidéo de la session.
|
||||
"""
|
||||
from cameras.models import ExportSession
|
||||
|
||||
try:
|
||||
session = ExportSession.objects.get(session_id=session_id)
|
||||
except ExportSession.DoesNotExist:
|
||||
logger.error("run_session_exports: session %s introuvable", session_id)
|
||||
return {"status": "error", "message": "Session introuvable"}
|
||||
|
||||
session.status = ExportSession.Status.RUNNING
|
||||
session.save(update_fields=["status"])
|
||||
|
||||
try:
|
||||
# Lancement en parallèle avec group Celery
|
||||
job = group(
|
||||
export_all_images.s(session_id),
|
||||
export_all_videos.s(session_id),
|
||||
).apply_async()
|
||||
|
||||
results = job.get(timeout=7200) # 2h max pour les deux
|
||||
|
||||
session.status = ExportSession.Status.DONE
|
||||
session.exported_at = timezone.now()
|
||||
session.save(update_fields=["status", "exported_at"])
|
||||
|
||||
return {"status": "success", "results": results}
|
||||
|
||||
except Exception as exc:
|
||||
session.status = ExportSession.Status.ERROR
|
||||
session.save(update_fields=["status"])
|
||||
logger.error("run_session_exports [%s]: %s", session_id, exc, exc_info=True)
|
||||
return {"status": "error", "message": str(exc)}
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def export_all_images(self, session_id: str):
|
||||
"""
|
||||
Export ZIP de toutes les images de la session.
|
||||
"""
|
||||
from cameras.models import ExportSession
|
||||
|
||||
session = ExportSession.objects.get(session_id=session_id)
|
||||
|
||||
return export_images_zip(
|
||||
session.camera_uuid,
|
||||
session.start_ts,
|
||||
session.end_ts,
|
||||
max_zip_size_mb = session.max_zip_size_mb,
|
||||
jpeg_quality = session.jpeg_quality,
|
||||
max_image_width = session.max_image_width,
|
||||
max_image_height = session.max_image_height,
|
||||
)
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def export_all_videos(self, session_id: str):
|
||||
"""
|
||||
Export MP4 de toutes les vidéos de la session.
|
||||
"""
|
||||
from cameras.models import ExportSession
|
||||
|
||||
session = ExportSession.objects.get(session_id=session_id)
|
||||
|
||||
return export_video_mp4(
|
||||
session.camera_uuid,
|
||||
session.start_ts,
|
||||
session.end_ts,
|
||||
frame_rate = session.frame_rate,
|
||||
max_video_size_mb = session.max_video_size_mb,
|
||||
max_width = session.max_width,
|
||||
max_height = session.max_height,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.utils import timezone
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Multi-well positions on the table for calibration and observation
|
||||
|
||||
MULTIWELL_POSITION = [
|
||||
('HG', _("HG-Haut gauche")),
|
||||
('HD', _("HD-Haut droit")),
|
||||
@@ -51,9 +51,7 @@ class Configuration(models.Model):
|
||||
grbl_xmax = models.FloatField(_("Grbl Xmax"), help_text=_("CNC Grbl Xmax en mm"), blank=False, default=350.0)
|
||||
grbl_ymax = models.FloatField(_("Grbl Ymax"), help_text=_("CNC Grbl Ymax en mm"), blank=False, default=250.0)
|
||||
# camera configuration
|
||||
use_rpicam = models.BooleanField(_("Utiliser rpicam"), help_text=_("Par défaaut. Sinon USB webcam"), default=True)
|
||||
capture_type = models.CharField(_("Capture"), help_text=_("Type de capture"), default='rpi', max_length=8, choices=CAPTURE_TYPE, null=True, blank=False)
|
||||
|
||||
webcam_device_index = models.PositiveSmallIntegerField(_("Index de la webcam"), help_text=_("Index de la webcam (0, 1, ...) si présente"), default=2)
|
||||
image_quality = models.PositiveSmallIntegerField(_("Qualité JPEG"), help_text=_("Qualité JPEG (1-100) pour les images exportées"), default=90)
|
||||
video_jpeg_quality = models.PositiveSmallIntegerField(_("Qualité JPEG pour les vidéos"), help_text=_("Qualité JPEG (1-100) pour les images extraites des vidéos"), default=90)
|
||||
@@ -68,9 +66,13 @@ 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)
|
||||
|
||||
#
|
||||
active = models.BooleanField(_("Actif"), default=False)
|
||||
|
||||
|
||||
@classmethod
|
||||
def active_config(cls):
|
||||
return Configuration.objects.filter(active=True).first()
|
||||
|
||||
class Meta:
|
||||
ordering = ['id', ]
|
||||
@@ -116,7 +118,7 @@ class MultiWell(models.Model):
|
||||
dy = models.FloatField(_("Pas Y"), help_text=_('Pas ou interval sur Y en mm'), blank=False, default=19.5)
|
||||
feed = models.PositiveIntegerField(_("Vitesse"), help_text=_('Vitesse déplacement en mm/mn '), blank=False, default=1000)
|
||||
|
||||
well_position = models.BooleanField(_("Positions"), help_text=_('Positions des puits générées ?. Non => efface WellPostion et recalcule les positions'), default=False)
|
||||
well_position = models.BooleanField(_("Positions"), help_text=_('Positions des puits générées ?. Non => efface WellPosition et recalcule les positions'), default=False)
|
||||
active = models.BooleanField(_("Active"), default=True)
|
||||
|
||||
|
||||
@@ -161,7 +163,7 @@ class MultiWell(models.Model):
|
||||
return f'{self.position}: {self.label}'
|
||||
|
||||
|
||||
class WellPostion(models.Model):
|
||||
class WellPosition(models.Model):
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
@@ -169,8 +171,13 @@ class WellPostion(models.Model):
|
||||
order = models.PositiveSmallIntegerField(_("Ordre"), help_text=_('Ordre de lecture du puit'), blank=False, default=0)
|
||||
x = models.FloatField(_("X"), help_text=_('Axe X en mm'), blank=False, default=10.0)
|
||||
y = models.FloatField(_("Y"), help_text=_('Axe Y en mm'), blank=False, default=10.0)
|
||||
px_per_mm = models.FloatField( default=50.0, verbose_name=_("Pixels par mm"), help_text=_("Facteur de calibration optique"))
|
||||
|
||||
|
||||
@classmethod
|
||||
def active_well(cls, multiwel, well):
|
||||
return WellPosition.objects.filter(multiwel_id=multiwel.id, well_id=well.id).first()
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
unique_together = ["multiwell", "well"]
|
||||
@@ -183,6 +190,8 @@ class WellPostion(models.Model):
|
||||
|
||||
@receiver(post_save, sender=MultiWell)
|
||||
def create_well_position(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
pass
|
||||
if not instance.well_position:
|
||||
row_order = instance.row_order.split(',')
|
||||
n = 0
|
||||
@@ -197,7 +206,7 @@ def create_well_position(sender, instance, created, **kwargs):
|
||||
try:
|
||||
name = f'{row_order[row]}{col+1}'
|
||||
well = Well.objects.get(name__exact=name)
|
||||
WellPostion.objects.update_or_create(
|
||||
WellPosition.objects.update_or_create(
|
||||
multiwell=instance,
|
||||
well=well,
|
||||
author=instance.author,
|
||||
@@ -210,9 +219,9 @@ def create_well_position(sender, instance, created, **kwargs):
|
||||
instance.save()
|
||||
|
||||
|
||||
class Observation(models.Model):
|
||||
title = models.CharField(_("Titre de l'observation"), max_length=100, null=True, blank=False)
|
||||
comment = models.TextField(_("Commentaires"), help_text=_("Descriptions de l'observations"), null=True, blank=True)
|
||||
class Experiment(models.Model):
|
||||
title = models.CharField(_("Titre de l'expérience"), max_length=100, null=True, blank=False)
|
||||
comment = models.TextField(_("Commentaires"), help_text=_("Descriptions de l'expérience"), null=True, blank=True)
|
||||
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)
|
||||
created = models.DateTimeField(_("Date de création"), default=timezone.now)
|
||||
@@ -221,28 +230,12 @@ class Observation(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created', ]
|
||||
verbose_name = _("Observation")
|
||||
verbose_name_plural = _("Observations")
|
||||
verbose_name = _("Expérience")
|
||||
verbose_name_plural = _("Expériences")
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.title}: {self.created} {self.multiwell.order}'
|
||||
|
||||
class ObservationMultiWellDetail(models.Model):
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
observation = models.ForeignKey(Observation, on_delete=models.CASCADE, related_name="multiwell_details" , null=True, blank=True)
|
||||
well = models.ForeignKey(Well, verbose_name="Puit", on_delete=models.CASCADE, related_name="observation_details", null=True, blank=True )
|
||||
detail = models.CharField("Détail", max_length=255)
|
||||
comment = models.TextField("Commentaire", blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['observation', 'well__name']
|
||||
unique_together = ["observation", "well"]
|
||||
verbose_name = _("Observation multi-puits détail")
|
||||
verbose_name_plural = _("Observations multi-puits détails")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.observation.title} - {self.well} - {self.detail}"
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
|
||||
@@ -252,7 +245,7 @@ class Session(models.Model):
|
||||
DONE = "done", _("Terminé")
|
||||
ERROR = "error", _("Erreur")
|
||||
|
||||
name = models.CharField(_("Nom de la session"), help_text=_("Session d'observations. 4 Multi-puits maximum"), max_length=100, null=True, blank=False)
|
||||
name = models.CharField(_("Nom de la session"), help_text=_("Session d'expérience. 4 Multi-puits maximum"), max_length=100, null=True, blank=False)
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
active = models.BooleanField(_("Active"), default=True)
|
||||
expected_export = models.DateTimeField(_("Date d'exportation"), help_text=_("Date d'exportation prévue"), null=True, blank=True)
|
||||
@@ -280,8 +273,8 @@ class Session(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created', ]
|
||||
verbose_name = _("Session d'observation")
|
||||
verbose_name_plural = _("Sessions d'observation")
|
||||
verbose_name = _("Session d'expérience")
|
||||
verbose_name_plural = _("Sessions d'expériences")
|
||||
|
||||
def __str__(self):
|
||||
state = _("Terminée") if not self.active else _("Active")
|
||||
@@ -346,20 +339,21 @@ def delete_periodic_task(sender, instance, **kwargs):
|
||||
if instance.scanning_task:
|
||||
instance.scanning_task.delete()
|
||||
|
||||
class SessionObservation(models.Model):
|
||||
|
||||
class SessionExperiment(models.Model):
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
observation = models.ForeignKey(Observation, verbose_name=_("Observation"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.SET_NULL, null=True, blank=True, related_name="session_experiments")
|
||||
|
||||
@classmethod
|
||||
def observation_by_session(cls, session_id, active=True):
|
||||
return [ ss.observation for ss in SessionObservation.objects.filter(session__id=session_id, session__active=active).order_by('observation__multiwell__order') ]
|
||||
def experiment_by_session(cls, session_id, active=True):
|
||||
return [ ss.experiment for ss in SessionExperiment.objects.filter(session__id=session_id, session__active=active).order_by('experiment__multiwell__order') ]
|
||||
|
||||
@classmethod
|
||||
def uuid_from_session(cls, sid):
|
||||
observations = [ss.observation for ss in SessionObservation.objects.filter(session__id=sid, session__active=False)]
|
||||
experiments = [ss.experiment for ss in SessionExperiment.objects.filter(session__id=sid, session__active=False)]
|
||||
uuid_list = []
|
||||
for obs in observations:
|
||||
for obs in experiments:
|
||||
row_def = obs.multiwell.row_def.split(',')
|
||||
for row in range(obs.multiwell.rows):
|
||||
for col in range(obs.multiwell.cols):
|
||||
@@ -369,9 +363,9 @@ class SessionObservation(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ['session',]
|
||||
unique_together = ["session", "observation"]
|
||||
verbose_name = _("Session observations")
|
||||
verbose_name_plural = _("Sessions observations")
|
||||
unique_together = ["session", "experiment"]
|
||||
verbose_name = _("Session expérience")
|
||||
verbose_name_plural = _("Sessions expériences")
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.session.name}'
|
||||
|
||||
@@ -89,6 +89,7 @@ class MultiWellManager:
|
||||
self._feed = feed or self.process.conf.calibration_default_feed
|
||||
self._step = step or self.process.conf.calibration_default_step
|
||||
self._duration = duration or self.process.conf.calibration_default_duration
|
||||
self.px_per_mm = 50.0
|
||||
|
||||
|
||||
def set_multiwell(self, position=None):
|
||||
@@ -97,7 +98,7 @@ class MultiWellManager:
|
||||
else:
|
||||
self.multiwell = models.MultiWell.by_position(position)
|
||||
|
||||
wells = models.WellPostion.objects.filter(multiwell_id=self.multiwell.id).order_by('order').all()
|
||||
wells = models.WellPosition.objects.filter(multiwell_id=self.multiwell.id).order_by('order').all()
|
||||
self.well_iterator = WellIterator(wells)
|
||||
|
||||
self.position = self.multiwell.position
|
||||
@@ -132,11 +133,11 @@ class MultiWellManager:
|
||||
logger.info(f"Arrêter l'enregistrement {uuid}")
|
||||
self.process.data.record = False
|
||||
self.process.data.uuid = None
|
||||
|
||||
|
||||
|
||||
def _grid_scanning(self, observation, xnext=0, ynext=0):
|
||||
multiwell = observation.multiwell
|
||||
wells = models.WellPostion.objects.filter(multiwell_id=multiwell.id).order_by('order').all()
|
||||
def _grid_scanning(self, experiment, xnext=0, ynext=0):
|
||||
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)
|
||||
|
||||
@@ -149,38 +150,48 @@ class MultiWellManager:
|
||||
uuid = f'{self.process.data.session}-{multiwell.position}-{wl.well.name}'
|
||||
self._grid_scanning_capture(uuid, multiwell.duration)
|
||||
|
||||
self.process._send(uuid=uuid)
|
||||
## change file
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self.process.cam._error_occured = True
|
||||
|
||||
self.process._send(scan_state=f"{uuid}: capture")
|
||||
|
||||
logger.info(f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})")
|
||||
self.cnc_controller.move_to(xnext, ynext, feed=multiwell.feed*2)
|
||||
|
||||
|
||||
def _start_scanning(self, session, observations):
|
||||
def _start_scanning(self, session, experiments):
|
||||
self.process.cam._aligner.debug = False
|
||||
|
||||
xynext = []
|
||||
for obs in observations:
|
||||
for obs in experiments:
|
||||
xynext.append((obs.multiwell.xbase, obs.multiwell.ybase))
|
||||
xynext.append((0, 0))
|
||||
|
||||
pos = 1
|
||||
self.process.data.session = session.id
|
||||
started = timezone.now()
|
||||
for obs in observations:
|
||||
for obs in experiments:
|
||||
if self.stop_playing.is_set():
|
||||
break
|
||||
obs.started = timezone.now()
|
||||
obs.save()
|
||||
|
||||
xnext, ynext = xynext[pos]
|
||||
pos +=1
|
||||
self._grid_scanning(obs, xnext=xnext, ynext=ynext)
|
||||
|
||||
obs.finished = timezone.now()
|
||||
obs.save()
|
||||
|
||||
session.finished = timezone.now()
|
||||
session.active = False
|
||||
session.scanning_task.enabled = False
|
||||
session.save()
|
||||
logger.info(f"==== Session {session.name} terminée à {session.finished} après {session.finished - started} secondes.")
|
||||
if self.stop_playing.is_set():
|
||||
msg = f"Session {session.name} abandonnée à {session.finished} après {session.finished - started} secondes."
|
||||
else:
|
||||
session.active = False
|
||||
if session.scanning_task:
|
||||
session.scanning_task.enabled = False
|
||||
session.save()
|
||||
msg = f"Session {session.name} terminée à {session.finished} après {session.finished - started} secondes."
|
||||
logger.info(msg)
|
||||
self.process._send(scan_state=msg)
|
||||
self.scan_thread = None
|
||||
|
||||
|
||||
@@ -188,7 +199,7 @@ class MultiWellManager:
|
||||
self.process.data.record = False
|
||||
self.stop_playing.set()
|
||||
self.well_iterator.reset()
|
||||
self.process.cam._aligner.debugg = False
|
||||
self.process.cam._aligner.debug = False
|
||||
|
||||
|
||||
def scanning(self, sid):
|
||||
@@ -196,8 +207,8 @@ class MultiWellManager:
|
||||
if self.scan_thread:
|
||||
return
|
||||
session = models.Session.objects.get(pk=sid)
|
||||
observations = models.SessionObservation.observation_by_session(sid)
|
||||
self.scan_thread = Thread(target=self._start_scanning, args=(session, observations, ), daemon=True).start()
|
||||
experiments = models.SessionExperiment.experiment_by_session(sid)
|
||||
self.scan_thread = Thread(target=self._start_scanning, args=(session, experiments, ), daemon=True).start()
|
||||
except Exception as e:
|
||||
print("MultiWellManager::scan error", e)
|
||||
|
||||
@@ -253,6 +264,7 @@ class MultiWellManager:
|
||||
if auto:
|
||||
msg = cam.align_detection["msg"]
|
||||
if cam.align_detection.get('detected'):
|
||||
|
||||
if cam.align_detection.get('action')=="grbl":
|
||||
self.cnc_controller.wait_for(settings.CALIBRATION_AUTO_TIMEOUT)
|
||||
dx_mm, dy_mm = cam.align_detection["offset_x_mm"], cam.align_detection["offset_y_mm"]
|
||||
@@ -265,6 +277,7 @@ class MultiWellManager:
|
||||
logger.info(msg)
|
||||
self.process._send(state='save', msg=msg)
|
||||
wl.x, wl.y = self.cnc_controller.x, self.cnc_controller.y
|
||||
wl.px_per_mm = cam.align_detection.get('px_per_mm')
|
||||
wl.save()
|
||||
if wl.order == 0:
|
||||
models.MultiWell.objects.filter(position__exact=self.position).update(xbase=wl.x, ybase=wl.y)
|
||||
@@ -371,6 +384,7 @@ class MultiWellManager:
|
||||
self._xbase, self._ybase = x, y
|
||||
wl = self.well_iterator.seek(0) # base puit 0
|
||||
wl.x, wl.y = x, y
|
||||
wl.px_per_mm = self.px_per_mm
|
||||
wl.save()
|
||||
|
||||
|
||||
|
||||
@@ -421,6 +421,7 @@ class ScannerProcess(Task):
|
||||
elif topic == 'center':
|
||||
dx_mm = self.cam.align_detection["offset_x_mm"]
|
||||
dy_mm = self.cam.align_detection["offset_y_mm"]
|
||||
self.manager.px_per_mm = self.cam.align_detection["px_per_mm"]
|
||||
self.grbl.move_to(self.grbl.x + dx_mm, self.grbl.y + dy_mm, feed=150)
|
||||
self._send(state='center', msg=self.cam.align_detection["msg"])
|
||||
continue
|
||||
|
||||
@@ -22,7 +22,7 @@ class ScannerManager {
|
||||
this.axial_pos = options.axial_pos;
|
||||
this.area_px = options.area_px;
|
||||
this.frame_count = options.frame_count;
|
||||
this.uuid = options.uuid;
|
||||
this.scan_state = options.scan_state;
|
||||
}
|
||||
|
||||
init_controls() {
|
||||
@@ -43,6 +43,8 @@ class ScannerManager {
|
||||
if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
|
||||
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.scan_state) { this.scan_state.textContent=payload.scan_state;}
|
||||
|
||||
if (payload.detected && use_tracking) {
|
||||
this.cx.textContent = payload.cx; this.cy.textContent = payload.cy;
|
||||
this.speed_px_s.textContent = payload.speed_px_s;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const mem_used = sId('mem-used');
|
||||
const disk_used = sId('disk-used');
|
||||
const ramdisk_used = sId('ramdisk-used');
|
||||
const swap_used = sId('swap-used');
|
||||
|
||||
let autoTimer = null;
|
||||
|
||||
@@ -12,10 +13,11 @@
|
||||
const r = await fetch(stats_endpoint, { credentials: 'same-origin' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const j = await r.json();
|
||||
//console.log(j);
|
||||
const cpu_percent = j.cpu_info.cpu_percent+'%'; cpu_used.style.setProperty("--cpu-used", cpu_percent); cpu_used.title=`Cpu: ${cpu_percent}`;
|
||||
const shm_length = j.shm.length; shm_used.style.setProperty("--shm_used", shm_length); shm_used.title= `Shm: ${shm_length}`;
|
||||
const virtual_memory = j.memory_info.virtual_memory.percent+'%'; mem_used.style.setProperty("--mem-used", virtual_memory); mem_used.title=`Mem: ${virtual_memory}`;
|
||||
const virtual_memory = j.memory_info.virtual_memory.percent+'%'; mem_used.style.setProperty("--mem-used", virtual_memory); mem_used.title=`Mem: ${virtual_memory}`;
|
||||
const swap_memory = j.memory_info.swap_memory.percent+'%'; swap_used.style.setProperty("--swap-used", swap_memory); swap_used.title=`swap: ${swap_memory}`;
|
||||
|
||||
const root_percent = j.disk_info.root.percent+'%'; disk_used.style.setProperty("--disk-used", root_percent); disk_used.title=`Disk: ${root_percent}`;
|
||||
let ramdisk_percent = "0%";
|
||||
if (! j.ramdisk_info) ramdisk_percent = j.ramdisk_info.percent+'%';
|
||||
|
||||
@@ -88,7 +88,7 @@ def export_all_images(session_id=None):
|
||||
sessions = [session_id]
|
||||
|
||||
for session_id in sessions:
|
||||
uuid_list = models.SessionObservation.uuid_from_session(session_id)
|
||||
uuid_list = models.SessionExperiment.uuid_from_session(session_id)
|
||||
job_zip = []
|
||||
for uuid in uuid_list:
|
||||
job = export_images.delay( # @UndefinedVariable
|
||||
@@ -129,7 +129,7 @@ def export_all_videos(session_id=None):
|
||||
sessions = [session_id]
|
||||
|
||||
for session_id in sessions:
|
||||
uuid_list = models.SessionObservation.uuid_from_session(session_id)
|
||||
uuid_list = models.SessionExperiment.uuid_from_session(session_id)
|
||||
job_mp4 = []
|
||||
for uuid in uuid_list:
|
||||
job = export_videos.delay( # @UndefinedVariable
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<div class="w3-light-blue w3-padding-large">
|
||||
<div id="shm-used" class="w3-green w3-round" style="height:0.5em; width: var(--shm-used, 0%);" title="{% trans 'Mémoire partagée' %}"></div>
|
||||
<div id="ramdisk-used" class="w3-purple w3-round" style="height:0.5em;width: var(--ramdisk-used, 0%); margin: 0.35em 0" title="{% trans 'Disque RAM' %}"></div>
|
||||
<div id="swap-used" class="w3-deep-purple w3-round" style="height:0.5em;width: var(--swap-used, 0%); margin: 0.35em 0" title="{% trans 'Swap disk' %}"></div>
|
||||
<div id="mem-used" class="w3-deep-orange w3-round" style="height:0.5em;width: var(--mem-used, 0%); margin: 0.35em 0" title="{% trans 'Mémoire RAM' %}"></div>
|
||||
<div id="cpu-used" class="w3-indigo w3-round" style="height:0.5em;width: var(--cpu-used, 0%); margin: 0.35em 0" title="{% trans 'Charge CPU' %}"></div>
|
||||
<div id="disk-used" class="w3-amber w3-round" style="height:0.5em;width: var(--disk-used, 0%); margin: 0.35em 0" title="{% trans 'Disque /' %}"></div>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<form method="post" class="w3-bar-item" action="">
|
||||
{% csrf_token %}
|
||||
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()">
|
||||
<option value="0">---- {% trans "Session d'observations" %}</option>
|
||||
<option value="0">---- {% trans "Session d'expérience" %}</option>
|
||||
{% for s in sessions %}
|
||||
<option value="{{ s.id }}" {% if s.id == cursid %}selected{% endif %}>{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if cursid %}
|
||||
{% multiwell_cards cursid observations %}
|
||||
{% multiwell_cards cursid experiments %}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if cursid %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="w3-row w3-row-padding w3-black">
|
||||
<div class="w3-col w3-center" style="width: 20%">
|
||||
<div class="w3-col w3-center" style="width: 30%">
|
||||
<div class="w3-col">
|
||||
<div>{% trans "Choix de la session" %}</div>
|
||||
<select id="_session" class="w3-select">
|
||||
@@ -50,9 +50,12 @@
|
||||
<div class="w3-margin-top w3-padding w3-small">
|
||||
<span id="_ts"></span>
|
||||
</div>
|
||||
<div class="w3-margin-top w3-padding w3-small">
|
||||
<span id="_scan_state" class="w3-text-amber w3-border"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w3-col" style="width: 80%">
|
||||
<div class="w3-col" style="width: 70%">
|
||||
{% include 'scanner/scan-image.html' %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +85,7 @@
|
||||
axial_pos : sId("_axial_pos"),
|
||||
area_px : sId("_area_px"),
|
||||
frame_count : sId("_count"),
|
||||
uuid: sId("_uuid")
|
||||
scan_state: sId("_scan_state")
|
||||
};
|
||||
</script>
|
||||
<script src="/static/scanner/js/main.js"></script>
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
<form method="post" class="w3-bar-item" action="">
|
||||
{% csrf_token %}
|
||||
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()">
|
||||
<option value="0">---- {% trans "Session d'observations" %}</option>
|
||||
<option value="0">---- {% trans "Session d'expériences" %}</option>
|
||||
{% for s in sessions %}
|
||||
<option value="{{ s.id }}" {% if s.id == cursid %}selected{% endif %}>{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if cursid %}
|
||||
{% multiwell_cards cursid observations %}
|
||||
{% multiwell_cards cursid experiments %}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if cursid %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<div class="w3-row">
|
||||
{% if use_tracking %}
|
||||
{% if conf.tracking %}
|
||||
<div class="w3-col w3-small" style="width:180px">
|
||||
<div>Num: <span id="_count"></span></div>
|
||||
<div>Aire: <span id="_area_px"></span></div>
|
||||
|
||||
@@ -5,9 +5,9 @@ from django.utils.html import mark_safe
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def multiwell_cards(sid, observations):
|
||||
def multiwell_cards(sid, experiments):
|
||||
multiwells = []
|
||||
for obs in observations:
|
||||
for obs in experiments:
|
||||
row_def = obs.multiwell.row_def.split(',')
|
||||
multiwells.append(
|
||||
f'''
|
||||
|
||||
@@ -11,16 +11,40 @@ from django.conf import settings
|
||||
from reduct.time import unix_timestamp_to_iso
|
||||
from modules.system_stats import get_cached_stats, start_background_updater
|
||||
from modules import reductstore
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .tasks import download_video, export_all_images, export_all_videos
|
||||
from .process import CameraRecordManager, cameraDB
|
||||
from . import models
|
||||
|
||||
@dataclass
|
||||
class DefaultConfig:
|
||||
sidebar_width: str = "25%"
|
||||
default_grid_columns: int = 3
|
||||
opencv_fourcc_format: str = 'mp4v'
|
||||
opencv_video_type: str = 'mp4'
|
||||
grbl_xmax: float = 350.0
|
||||
grbl_ymax: float = 250.0
|
||||
capture_type: str = 'rpi'
|
||||
webcam_device_index: int = 2
|
||||
image_quality: int = 90
|
||||
video_jpeg_quality: int = 90
|
||||
video_frame_rate: int = 5.0
|
||||
video_width_capture: int = 2028
|
||||
video_height_capture: int = 1520
|
||||
calibration_crop_radius: int = 500
|
||||
calibration_default_multiwell: str = 'HD'
|
||||
calibration_default_feed: int = 1000
|
||||
calibration_default_step: float = 1.0
|
||||
calibration_default_duration: float = 3.0
|
||||
tracking: bool = False
|
||||
|
||||
|
||||
default_conf = DefaultConfig()
|
||||
record_manager = CameraRecordManager(cameraDB)
|
||||
start_background_updater()
|
||||
|
||||
|
||||
|
||||
|
||||
@require_GET
|
||||
def stats_view(request):
|
||||
"""
|
||||
@@ -34,12 +58,12 @@ def stats_view(request):
|
||||
|
||||
def global_context(request, **ctx):
|
||||
default_multiwell = models.MultiWell.objects.filter(default=True).first()
|
||||
conf = models.Configuration.active_config() or default_conf
|
||||
return dict(
|
||||
app_title=settings.APP_TITLE,
|
||||
app_sub_title=settings.APP_SUB_TITLE,
|
||||
domain_server=settings.DOMAIN_SERVER,
|
||||
use_tracking=settings.TRACKING,
|
||||
conf=models.Configuration.objects.filter(active=True).first(),
|
||||
conf=conf,
|
||||
default_position = default_multiwell.position or 'HD',
|
||||
**ctx
|
||||
)
|
||||
@@ -131,7 +155,7 @@ def images_view(request):
|
||||
ctx = dict(
|
||||
choice_title=_("Gestionnaire d'images"),
|
||||
sessions=models.Session.objects.filter(active=False).all(),
|
||||
observations=models.SessionObservation.observation_by_session(cursid, active=False),
|
||||
experiments=models.SessionExperiment.experiment_by_session(cursid, active=False),
|
||||
cursid=int(cursid),
|
||||
images=images,
|
||||
uuid=uuid,
|
||||
@@ -174,7 +198,7 @@ def replay_view(request):
|
||||
choice_title=_("Gestionnaire de vidéos"),
|
||||
ws_route=settings.REPLAY_WEBSOCKET_ROUTE,
|
||||
sessions=models.Session.objects.filter(active=False).all(),
|
||||
observations=models.SessionObservation.observation_by_session(cursid, active=False),
|
||||
experiments=models.SessionExperiment.experiment_by_session(cursid, active=False),
|
||||
cursid=int(cursid),
|
||||
image=image,
|
||||
uuid=uuid,
|
||||
|
||||
Reference in New Issue
Block a user