scanning
This commit is contained in:
@@ -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', )
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
||||
if (payload.state) {
|
||||
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', `<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.buttons) { this.well_btn.innerHTML = payload.buttons; }
|
||||
if (payload.buttons) {
|
||||
this.well_btn.innerHTML = payload.buttons;
|
||||
const span = this.crop.querySelector("span.crop"); span.style.color = '#f0f';
|
||||
}
|
||||
if (payload.current >= 0) {
|
||||
document.querySelectorAll('button.w3-button.well').forEach(btn => {
|
||||
document.querySelectorAll('button.w3-btn.well').forEach(btn => {
|
||||
if (btn.value==payload.current) { btn.classList.add('w3-green'); return; }
|
||||
btn.classList.remove('w3-green');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()" title="{% trans "Ensemble d'expériences" %}">
|
||||
<option value="0">---- {% trans "Session" %}</option>
|
||||
{% for s in sessions %}
|
||||
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s.name }}</option>
|
||||
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="w3-margin-left w3-margin-bottom">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()">
|
||||
<option value="0">---- {% trans "Session" %}</option>
|
||||
{% for s in sessions %}
|
||||
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s.name }}</option>
|
||||
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="w3-margin-left w3-margin-bottom">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div>{% trans "Choix de la session" %}</div>
|
||||
<select id="_session" class="w3-select">
|
||||
{% for s in sessions %}
|
||||
<option value="{{ s.id }}">{{ s.name }}</option>
|
||||
<option value="{{ s.id }}">{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -28,26 +28,28 @@
|
||||
<div class="w3-row w3-row-padding">
|
||||
<div class="w3-half">
|
||||
<button id="_median" class="w3-button w3-dark-xlight w3-round-large w3-block">
|
||||
{% trans 'Axes' %} <br><i class="fa-solid fa-crosshairs w3-text-amber w3-xlarge"></i>
|
||||
<span class="median">{% trans 'Axes' %}</span><br><i class="fa-solid fa-crosshairs w3-text-amber w3-xlarge"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w3-half">
|
||||
<button id="_crop" class="w3-button w3-dark-xlight w3-round-large w3-block">
|
||||
{% trans 'Recadrer' %}<br><i class="fa-solid fa-crop w3-text-amber w3-xlarge"></i>
|
||||
<span class="crop" style="color: red">{% trans 'Recadrer' %}</span><br><i class="fa-solid fa-crop w3-text-amber w3-xlarge"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w3-row w3-row-padding">
|
||||
<div class="w3-twothird w3-center">
|
||||
<div class="{% if conf.scan_simulation %}w3-twothird{% else %}w3-col{% endif %} w3-center">
|
||||
<button id="_scan" class="w3-button w3-light-blue w3-round-large w3-margin-top w3-block">
|
||||
{% trans 'Lancer' %}<br>{% trans 'le balayage' %}<br><i class="fa-solid fa-circle-check w3-text-blue w3-xlarge"></i>
|
||||
<span class="w3-bold">{% trans 'Lancer' %}<br>{% trans 'le balayage' %}</span><br><i class="fa-solid fa-circle-check w3-text-blue w3-xlarge"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% if conf.scan_simulation %}
|
||||
<div class="w3-third w3-center">
|
||||
<button id="_simulate" class="w3-button w3-dark-xlight w3-round-large w3-margin-top w3-block">
|
||||
{% trans 'Simuler' %}<br>{% trans 'le balayage' %}<br><i class="fa-solid fa-triangle-exclamation w3-text-orange w3-xlarge"></i>
|
||||
<span class="w3-bold">{% trans 'Simuler' %}<br>{% trans 'le balayage' %}</span><br><i class="fa-solid fa-triangle-exclamation w3-text-orange w3-xlarge"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="w3-col">
|
||||
<div id="_well_btn" class="w3-margin-top"></div>
|
||||
</div>
|
||||
@@ -58,10 +60,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="w3-col">
|
||||
<div class="w3-margin-top w3-padding w3-small">
|
||||
<div class="w3-margin-top w3-padding-small w3-small">
|
||||
<span id="_ts"></span>
|
||||
</div>
|
||||
<div class="w3-margin-top w3-small">
|
||||
<div class="w3-margin-top w3-small w3-center w3-justify">
|
||||
<span id="_scan_state" class="w3-text-amber w3-padding"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.http import JsonResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.conf import settings
|
||||
from reduct.time import unix_timestamp_to_iso
|
||||
from modules.system_stats import get_cached_stats, start_background_updater
|
||||
@@ -20,6 +20,10 @@ from .constants import ScannerConstants
|
||||
record_manager = CameraRecordManager(cameraDB)
|
||||
start_background_updater()
|
||||
|
||||
|
||||
def is_staff_or_admin(user):
|
||||
return user.is_staff or user.is_superuser
|
||||
|
||||
|
||||
@require_GET
|
||||
def stats_view(request):
|
||||
@@ -48,10 +52,12 @@ def global_context(request, **ctx):
|
||||
)
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def admin_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/'))
|
||||
|
||||
@login_required
|
||||
|
||||
def admin_session_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/session/'))
|
||||
|
||||
@@ -68,26 +74,33 @@ def admin_periodictask_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/django_celery_beat/periodictask/'))
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def reductstore_view(request):
|
||||
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:8383/'))
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def adminer_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}/adminer/'))
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def portainer_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9000/'))
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def supervisor_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/'))
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def supervisor_worker(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:services'))
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_staff_or_admin)
|
||||
def supervisor_scheduler(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:planification'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user