diff --git a/test_tube_scanner/home/static/js/w3-custom.js b/test_tube_scanner/home/static/js/w3-custom.js index 1b7642b..5a37119 100644 --- a/test_tube_scanner/home/static/js/w3-custom.js +++ b/test_tube_scanner/home/static/js/w3-custom.js @@ -1,6 +1,6 @@ /* ** custom.js -** GNU GENERAL PUBLIC LICENSE: (c) DD miraceti.net return document.querySelectorAll(id); +** GNU GENERAL PUBLIC LICENSE: (c) DD miraceti.net */ let s = function(sel) { return document.querySelector(sel); }; let ss = function(sel) { return document.querySelectorAll(sel); }; diff --git a/test_tube_scanner/scanner/admin.py b/test_tube_scanner/scanner/admin.py index d2336b1..96804e3 100644 --- a/test_tube_scanner/scanner/admin.py +++ b/test_tube_scanner/scanner/admin.py @@ -66,14 +66,15 @@ class WellPositionAdmin(admin.ModelAdmin): list_display = ('multiwell__position', 'well__name', 'order', 'x', 'y', 'px_per_mm', 'author',) -#class ExperimentConfigInline(admin.TabularInline): -# model = models.ExperimentConfig -# extra = 0 +class ExperimentWellInline(admin.TabularInline): + model = models.ExperimentWell + extra = 0 + class ExperimentAdmin(admin.ModelAdmin): - #inlines = (ExperimenConfigInline,) + inlines = (ExperimentWellInline, ) list_filter = ('session_experiments__session', 'author', ) - list_display = ('title', 'author', 'identifier', 'multiwell', 'created', 'started', 'finished') + list_display = ('title', 'author', 'identifier', 'duration', 'multiwell', 'created', 'started', 'finished') readonly_fields = ('created', 'identifier', 'started', 'finished', ) diff --git a/test_tube_scanner/scanner/models.py b/test_tube_scanner/scanner/models.py index 662d557..b8b3f16 100644 --- a/test_tube_scanner/scanner/models.py +++ b/test_tube_scanner/scanner/models.py @@ -238,6 +238,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) @@ -400,3 +401,29 @@ class SessionExperiment(models.Model): return f'{self.session.name}' +class ExperimentWell(models.Model): + author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True) + experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.SET_NULL, null=True, blank=True, related_name="experimentwell") + well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True, related_name="wellexperiment") + active = models.BooleanField(_("Active"), default=True) + + @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') ] + + class Meta: + ordering = ['experiment',] + unique_together = ["experiment", "well", ] + verbose_name = _("Expérience puit") + verbose_name_plural = _("Expériences puitd") + + def __str__(self): + return f'{self.experiment.title}' + + +@receiver(post_save, sender=Experiment) +def create_experiment_well(sender, instance, created, **kwargs): + wells = Well.objects.all() + for well in wells: + ExperimentWell.objects.get_or_create(experiment=instance, well=well, author=instance.author, defaults={'active':True}) + diff --git a/test_tube_scanner/scanner/multiwell.py b/test_tube_scanner/scanner/multiwell.py index 2259d34..88b2afe 100644 --- a/test_tube_scanner/scanner/multiwell.py +++ b/test_tube_scanner/scanner/multiwell.py @@ -137,11 +137,11 @@ class MultiWellManager: return self.multiwell.config() - def multiwell_buttons(self): + def multiwell_buttons(self, btn_class="w3-button", onclick=''' onclick="goto_well(this)"'''): multiwells = [] multiwells.append('''
''') for wl in self.well_iterator: - multiwells.append(f"""""") + multiwells.append(f"""""") multiwells.append('''
''') self.well_iterator.reset() return mark_safe("\n".join(multiwells)) @@ -152,22 +152,27 @@ class MultiWellManager: try: well = well_position.well multiwell = experiment.multiwell + if self.process.use_tracking: 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, draw_contours=False) - + + ## create uuid for this capture uuid = f'{self.process.data.session}-{multiwell.position}-{well.name}' + ## start recording self.process.data.uuid = uuid if not simulate: self.process.data.record = True + self.process._send(current=well_position.order) start = time.monotonic() while not self.stop_playing.is_set(): - if time.monotonic() - start > multiwell.duration: + ## stop after duration in experiemnt now + if time.monotonic() - start > experiment.duration: break self.cnc_controller.wait_for(0.1) @@ -190,20 +195,27 @@ class MultiWellManager: self.process.cam._error_occured = True logger.info(f"Simulating capture with file {vf}") + + def _is_well_valid(self, welposition): + names = models.ExperimentWell.wellname_by_experiment(welposition.experiment.id) + if welposition.well.name not in names: + return False + return True 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() + 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 wells: + for wl in wellpositions: if self.stop_playing.is_set(): break - self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed) + if not self._is_well_valid(wl): + continue - ## change file + self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed) if self.process.conf.capture_type == 'file': self._capture_file_simulation(wl.well.name) @@ -305,9 +317,21 @@ class MultiWellManager: 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.well.name}: ({wl.x}, {wl.y})"} - - + return {"state": "goto", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"} + + + def goto_xy(self): + wl = self.well_iterator.seek(0) + if self.process.conf.capture_type == 'file': + self._capture_file_simulation(wl.well.name) + self.cnc_controller.move_to(self.xbase, self.ybase, feed=self.feed) + + def goto_0(self): + self.well_iterator.reset() + if self.process.conf.capture_type == 'file': + self._capture_file_simulation('zero') + self.cnc_controller.move_to(0, 0, feed=self.feed*2) + def set_well_position(self): wl = self.well_iterator.get_current() if wl: @@ -329,8 +353,12 @@ class MultiWellManager: for wl in self.well_iterator: if self.stop_playing.is_set(): break + self.cnc_controller.wait_for(2.0) self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed) + ## change file + if self.process.conf.capture_type == 'file': + self._capture_file_simulation(wl.well.name) self.process._send(current=wl.order) start = time.monotonic() @@ -374,9 +402,9 @@ class MultiWellManager: self.process.cam._aligner.debug = False logger.info(f"Scan terminé — retour à l'origine (X=0, Y=0) en {int(time.monotonic()-start_test)} s") - self.cnc_controller.move_to(0, 0, feed=self.multiwell.feed*2) - - + self.goto_0() + self.process._send(current=self.get_well_order()) + def scan_test(self, auto=False): if self.test_thread: return @@ -467,12 +495,12 @@ class MultiWellManager: """ Active / désactive le mode debug sur le stream.""" aligner = self.process.cam._aligner aligner.debug = not aligner.debug - return {"state": "debug", "msg": f"Debug: {aligner.debug}"} + return {"state": "debug", "value": aligner.debug, "msg": f"Debug: {aligner.debug}"} def set_calib_debug(self, value=True): """ Active / désactive le mode debug sur le stream.""" aligner = self.process.cam._aligner aligner.debug = value - return {"state": "debug", "msg": f"Debug: {aligner.debug}"} + return {"state": "debug", "value": aligner.debug, "msg": f"Debug: {aligner.debug}"} diff --git a/test_tube_scanner/scanner/process.py b/test_tube_scanner/scanner/process.py index 124597a..951b00b 100644 --- a/test_tube_scanner/scanner/process.py +++ b/test_tube_scanner/scanner/process.py @@ -373,7 +373,9 @@ 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="") + self._send(buttons=buttons) + elif topic == 'scan' or topic == 'simulate': logger.info(f"==== Scan {cmd}") sid = cmd.get("session", '0') @@ -389,10 +391,13 @@ class ScannerProcess(Task): self._send(state=topic, msg=str(_('Balayage démarré...'))) except Exception as e: logger.error(f"Scan error: {e}") - self._send(state='error', msg=str(_('Erreur lors du démarrage du balayage...'))) - + self._send(state='error', msg=str(_('Erreur lors du démarrage du balayage...'))) continue - + + self._send( + buttons=buttons, + current=self.manager.get_well_order(), + ) elif cmd["type"]=="calibrate": topic = cmd.get("topic") value = cmd.get("value") @@ -424,11 +429,13 @@ class ScannerProcess(Task): elif topic == 'median': self.cam._active_median = not self.cam._active_median + self._send(state="median", value=self.cam._active_median, msg=f"Median: {self.cam._active_median}") continue elif topic == 'crop': self.cam._active_crop = not self.cam._active_crop self.cam.set_circular_crop(self.crop) if self.cam._active_crop else self.cam.set_circular_crop(None) + self._send(state="crop", value=self.cam._active_crop, msg=f"Crop: {self.cam._active_crop}") continue elif topic == 'crop_radius': @@ -452,18 +459,11 @@ class ScannerProcess(Task): self.manager.duration = float(value) 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') - + self.manager.goto_0() + elif topic == 'goto_xy': - self.grbl.move_to(self.manager.xbase, self.manager.ybase, feed=self.manager.feed) - wl = self.manager.well_iterator.seek(0) - if self.conf.capture_type == 'file': - self.manager._capture_file_simulation(wl.well.name) - + self.manager.goto_xy() + elif topic == 'xy_base': self.manager.set_position() @@ -488,7 +488,6 @@ class ScannerProcess(Task): elif topic == 'halt': self.manager.halt_scanning() - elif topic == 'calib_debug': msg = self.manager.calib_toggle_debug() self._send(**msg) diff --git a/test_tube_scanner/scanner/static/scanner/css/scanning.css b/test_tube_scanner/scanner/static/scanner/css/scanning.css index 8b13789..97ea60e 100644 --- a/test_tube_scanner/scanner/static/scanner/css/scanning.css +++ b/test_tube_scanner/scanner/static/scanner/css/scanning.css @@ -1 +1,11 @@ +.well { + padding: 0.2em; +} + +.well-btn { + display: grid; + grid-template-columns: repeat(6, 1fr); + justify-items: center; + align-items: center; +} diff --git a/test_tube_scanner/scanner/static/scanner/js/calibration.js b/test_tube_scanner/scanner/static/scanner/js/calibration.js index 3e97683..6ff1d8c 100644 --- a/test_tube_scanner/scanner/static/scanner/js/calibration.js +++ b/test_tube_scanner/scanner/static/scanner/js/calibration.js @@ -101,16 +101,25 @@ class ScannerManager { if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; } if (payload.xbase) { this.xbase.textContent = payload.xbase; this.ybase.textContent = payload.ybase; } if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); } - if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `
  • [ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}
  • `); } + if (payload.state) { + if (payload.state == 'debug') { + const span = this.calib_debug.querySelector("span.debug"); span.style.color = payload.value ? '#f0f': '#fff'; + } else if (payload.state == 'median') { + const span = this.median.querySelector("span.median"); span.style.color = payload.value ? '#f0f' : '#fff'; + } else if (payload.state == 'crop') { + const span = this.crop.querySelector("span.crop"); span.style.color = payload.value ? '#f0f' : '#fff'; + } + this.debug.insertAdjacentHTML('afterbegin', `
  • [ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}
  • `); + } if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); } - + if (payload.buttons) { this.well_btn.innerHTML = payload.buttons; } if (payload.current >= 0) { document.querySelectorAll('button.w3-button.well').forEach(btn => { if (btn.value==payload.current) { btn.classList.add('w3-green'); return; } btn.classList.remove('w3-green'); }); - } + } } catch(e) { console.log(e); } } diff --git a/test_tube_scanner/scanner/static/scanner/js/scanning.js b/test_tube_scanner/scanner/static/scanner/js/scanning.js index 5a404cc..9497428 100644 --- a/test_tube_scanner/scanner/static/scanner/js/scanning.js +++ b/test_tube_scanner/scanner/static/scanner/js/scanning.js @@ -24,6 +24,7 @@ class ScannerManager { this.frame_count = options.frame_count; this.scan_state = options.scan_state; this.sim_bt = options.simulate; + this.well_btn = options.well_btn; } init_controls() { @@ -46,15 +47,15 @@ class ScannerManager { if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `
  • [ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}
  • `); } 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; - 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 => { + if (btn.value==payload.current) { btn.classList.add('w3-green'); return; } + btn.classList.remove('w3-green'); + }); + } + } catch(e) { console.log(e); } } diff --git a/test_tube_scanner/scanner/templates/scanner/calibration.html b/test_tube_scanner/scanner/templates/scanner/calibration.html index f3cb720..db717ed 100644 --- a/test_tube_scanner/scanner/templates/scanner/calibration.html +++ b/test_tube_scanner/scanner/templates/scanner/calibration.html @@ -85,7 +85,8 @@
    -
    +
    +
    diff --git a/test_tube_scanner/scanner/templates/scanner/scanning.html b/test_tube_scanner/scanner/templates/scanner/scanning.html index 7d256ee..7ce67c5 100644 --- a/test_tube_scanner/scanner/templates/scanner/scanning.html +++ b/test_tube_scanner/scanner/templates/scanner/scanning.html @@ -48,6 +48,9 @@ {% trans 'Simuler' %}
    {% trans 'le balayage' %}
    +
    +
    +