diff --git a/test_tube_scanner/scanner/admin.py b/test_tube_scanner/scanner/admin.py index 96804e3..e4322c6 100644 --- a/test_tube_scanner/scanner/admin.py +++ b/test_tube_scanner/scanner/admin.py @@ -26,10 +26,10 @@ class ConfigurationAdmin(admin.ModelAdmin): "classes": ("collapse",), }), (_("Camera"), { - "fields": ("capture_type", "webcam_device_index", "image_quality", "video_jpeg_quality", "video_frame_rate", "video_width_capture", "video_height_capture"), + "fields": ("scan_simulation", "capture_type", "webcam_device_index", "image_quality", "video_jpeg_quality", "video_frame_rate", "video_width_capture", "video_height_capture"), "classes": ("collapse",), }), - (_("Calibration"), { + (_("Calibration / Balayage"), { "fields": ("calibration_crop_radius", "calibration_default_multiwell", "calibration_default_feed", "calibration_default_step", "calibration_default_duration"), "classes": ("collapse",), }), @@ -64,13 +64,14 @@ class MultiWellAdmin(admin.ModelAdmin): class WellPositionAdmin(admin.ModelAdmin): list_filter = ('author', 'multiwell') list_display = ('multiwell__position', 'well__name', 'order', 'x', 'y', 'px_per_mm', 'author',) - - + + class ExperimentWellInline(admin.TabularInline): model = models.ExperimentWell extra = 0 - - + #ordering = ('experiment__multiwell__wellposition__order',) + + class ExperimentAdmin(admin.ModelAdmin): inlines = (ExperimentWellInline, ) list_filter = ('session_experiments__session', 'author', ) diff --git a/test_tube_scanner/scanner/constants.py b/test_tube_scanner/scanner/constants.py index acec259..d9128cd 100644 --- a/test_tube_scanner/scanner/constants.py +++ b/test_tube_scanner/scanner/constants.py @@ -22,6 +22,7 @@ class DefaultConfig: video_frame_rate: int = 5.0 video_width_capture: int = 2028 video_height_capture: int = 1520 + scan_simulation: bool = False calibration_crop_radius: int = 500 calibration_default_multiwell: str = 'HD' calibration_default_feed: int = 1000 diff --git a/test_tube_scanner/scanner/models.py b/test_tube_scanner/scanner/models.py index b8b3f16..fdb77b4 100644 --- a/test_tube_scanner/scanner/models.py +++ b/test_tube_scanner/scanner/models.py @@ -64,6 +64,7 @@ class Configuration(models.Model): video_width_capture = models.PositiveSmallIntegerField(_("Largeur de capture vidéo"), help_text=_("Largeur de capture vidéo en pixels"), default=1280) video_height_capture = models.PositiveSmallIntegerField(_("Hauteur de capture vidéo"), help_text=_("Hauteur de capture vidéo en pixels"), default=720) # Calibration + scan_simulation = models.BooleanField(_("Simuler balayage"), help_text=_("Autorise la simulation du balayage"), default=False) calibration_crop_radius = models.PositiveSmallIntegerField(_("Rayon de découpe pour la calibration"), help_text=_("Rayon en pixels pour découper les images de calibration en px"), default=150) calibration_default_multiwell = models.CharField(_("Multi-puits de calibration par défaut"), help_text=_("Position du multi-puits de calibration par défaut"), max_length=8, choices=MULTIWELL_POSITION, default='HG') calibration_default_feed = models.PositiveIntegerField(_("Vitesse de calibration"), help_text=_("Vitesse de déplacement pour la calibration en mm/mn"), default=1000) @@ -308,7 +309,7 @@ class Session(models.Model): def __str__(self): state = _("Terminée") if not self.active else _("Active") - return f'{self.name}: {state}' + return f'{self.id}: {self.name} {state}' @receiver(post_save, sender=Session) @@ -409,10 +410,10 @@ class ExperimentWell(models.Model): @classmethod def wellname_by_experiment(cls, experiment_id): - return [ ew.well.name for ew in ExperimentWell.objects.filter(experiment__id=experiment_id, active=True).order_by('well__order') ] + return [ ew.well.name for ew in ExperimentWell.objects.filter(experiment__id=experiment_id, active=True).order_by('well__name') ] class Meta: - ordering = ['experiment',] + ordering = ['experiment', 'well'] unique_together = ["experiment", "well", ] verbose_name = _("Expérience puit") verbose_name_plural = _("Expériences puitd") diff --git a/test_tube_scanner/scanner/multiwell.py b/test_tube_scanner/scanner/multiwell.py index 88b2afe..683b049 100644 --- a/test_tube_scanner/scanner/multiwell.py +++ b/test_tube_scanner/scanner/multiwell.py @@ -78,8 +78,7 @@ class MultiWellManager: def __init__(self, process): self.process = process - self.cnc_controller = process.grbl - + self.cnc_controller = process.grbl logger.info(f"MultiWellManager initialized with CNC controller: {self.cnc_controller}") self.stop_playing = Event() @@ -99,8 +98,6 @@ class MultiWellManager: min_contour_dist_px = self.process.conf.min_contour_dist_px, ) - - def set_tracker_config(self): self.tracker_config = dict( tube_axis = settings.TRACKER_TUBE_AXIS, @@ -169,6 +166,8 @@ class MultiWellManager: self.process.data.record = True self.process._send(current=well_position.order) + logger.info(f"Starting capture for {uuid} ordre: {well_position.order}") + start = time.monotonic() while not self.stop_playing.is_set(): ## stop after duration in experiemnt now @@ -196,8 +195,8 @@ class MultiWellManager: logger.info(f"Simulating capture with file {vf}") - def _is_well_valid(self, welposition): - names = models.ExperimentWell.wellname_by_experiment(welposition.experiment.id) + def _is_well_valid(self, welposition, experiment): + names = models.ExperimentWell.wellname_by_experiment(experiment.id) if welposition.well.name not in names: return False return True @@ -206,13 +205,14 @@ class MultiWellManager: try: multiwell = experiment.multiwell wellpositions = models.WellPosition.objects.filter(multiwell_id=multiwell.id).order_by('order').all() + cam = self.process.cam cam._aligner.set_tube_diameter(multiwell.diameter) for wl in wellpositions: if self.stop_playing.is_set(): break - if not self._is_well_valid(wl): + if not self._is_well_valid(wl, experiment): continue self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed) @@ -223,17 +223,18 @@ class MultiWellManager: msg =f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})" logger.info(msg) self.process._send(state='scan_finished', msg=msg) - + return True except Exception as e: msg = f"Error during grid scanning - {e}" logger.error(msg) self.process._send(state='error', msg=msg) - + return False finally: self.cnc_controller.move_to(xnext, ynext, feed=multiwell.feed*2) def _start_scanning(self, session, experiments, simulate=False): + result = False try: self.process.cam._aligner.debug = False self.stop_playing.clear() @@ -247,18 +248,21 @@ class MultiWellManager: self.process.data.session = session.id started = timezone.now() for obs in experiments: + + logger.warning(f"Starting scan for {obs} (well {pos}/{len(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, simulate=simulate) + result = self._grid_scanning(obs, xnext=xnext, ynext=ynext, simulate=simulate) obs.finished = timezone.now() obs.save() session.finished = timezone.now() - if self.stop_playing.is_set(): + if self.stop_playing.is_set() or not result: msg = f"Session {session.name} abandonnée à {session.finished} après {session.finished - started} secondes." else: if not simulate: @@ -273,6 +277,8 @@ class MultiWellManager: logger.error("Error during scanning process", e) finally: self.scan_thread = None + self.goto_0() + self.process._send(current=self.get_well_order()) def halt_scanning(self): diff --git a/test_tube_scanner/scanner/process.py b/test_tube_scanner/scanner/process.py index 951b00b..f37a087 100644 --- a/test_tube_scanner/scanner/process.py +++ b/test_tube_scanner/scanner/process.py @@ -373,7 +373,7 @@ class ScannerProcess(Task): self.cam._active_median = False self.grbl.go_origin(feed=self.manager.feed) self.cam.set_draw_contours(False) - buttons = self.manager.multiwell_buttons(btn_class="w3-btn", onclick="") + buttons = self.manager.multiwell_buttons(btn_class="w3-btn well", onclick="") self._send(buttons=buttons) elif topic == 'scan' or topic == 'simulate': diff --git a/test_tube_scanner/scanner/static/scanner/js/scanning.js b/test_tube_scanner/scanner/static/scanner/js/scanning.js index 9497428..f3941ac 100644 --- a/test_tube_scanner/scanner/static/scanner/js/scanning.js +++ b/test_tube_scanner/scanner/static/scanner/js/scanning.js @@ -23,7 +23,7 @@ class ScannerManager { this.area_px = options.area_px; this.frame_count = options.frame_count; this.scan_state = options.scan_state; - this.sim_bt = options.simulate; + this.sim_bt = options.simulate || null; this.well_btn = options.well_btn; } @@ -32,7 +32,9 @@ class ScannerManager { this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); }); this.scan_bt.addEventListener('click', (e) => { this.scan(); }); this.halt_bt.addEventListener('click', (e) => { this.halt(); }); - this.sim_bt.addEventListener('click', (e) => { this.simulate(); }); + + if (this.sim_bt) + this.sim_bt.addEventListener('click', (e) => { this.simulate(); }); } registerSocket(socket) { @@ -44,18 +46,29 @@ class ScannerManager { try { if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; } if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); } - if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `