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 @@
-
+
+
+
{% trans 'ARRET' %}
@@ -94,7 +97,8 @@
area_px : sId("_area_px"),
frame_count : sId("_count"),
scan_state : sId("_scan_state"),
- simulate : sId("_simulate")
+ simulate : sId("_simulate"),
+ well_btn : sId("_well_btn")
};