planarian

This commit is contained in:
2026-04-27 23:28:41 +02:00
parent 922d89bf8d
commit cf0230a28b
34 changed files with 2832 additions and 191 deletions
+20 -18
View File
@@ -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)
-88
View File
@@ -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,
)
+35 -41
View File
@@ -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}'
+33 -19
View File
@@ -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()
+1
View File
@@ -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+'%';
+2 -2
View File
@@ -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'''
+30 -6
View File
@@ -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,