simulation
This commit is contained in:
@@ -239,6 +239,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
|
|
||||||
LOGGING_FILE = config('LOGGING_FILE')
|
LOGGING_FILE = config('LOGGING_FILE')
|
||||||
IS_LOGGING = config('IS_LOGGING', cast=bool)
|
IS_LOGGING = config('IS_LOGGING', cast=bool)
|
||||||
|
IS_LOGGING = False
|
||||||
|
|
||||||
LOGGING = None if not IS_LOGGING else {
|
LOGGING = None if not IS_LOGGING else {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
|
|
||||||
self._tracker = None
|
self._tracker = None
|
||||||
self._metrics = None
|
self._metrics = None
|
||||||
self._paramss = None
|
self._params = None
|
||||||
|
|
||||||
# Tracker générique, pour simulation
|
# Tracker générique, pour simulation
|
||||||
self.on_test_well_change(**self.DEFAULT_TRACKER_CONFIG)
|
self.on_test_well_change(**self.DEFAULT_TRACKER_CONFIG)
|
||||||
@@ -94,14 +94,17 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
dead_zone_px = 5, # en-dessous → rien à faire
|
dead_zone_px = 5, # en-dessous → rien à faire
|
||||||
display = display,
|
display = display,
|
||||||
)
|
)
|
||||||
self.align_detection = None # résultat du test
|
self.align_detection = None # résultat du test
|
||||||
|
|
||||||
|
|
||||||
def on_test_well_change(self, **cfg):
|
def on_test_well_change(self, **cfg):
|
||||||
if self.use_tracking and cfg:
|
try:
|
||||||
self._tracker = PlanarianTracker(**cfg)
|
if self.use_tracking and cfg:
|
||||||
logger.warning(f"New tracker conf: {cfg}")
|
self._tracker = PlanarianTracker(**cfg)
|
||||||
|
logger.info("Tracker de test créé avec conf : %s", cfg)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating tracker with conf {cfg}: {e}")
|
||||||
|
self._tracker = None
|
||||||
|
|
||||||
def on_well_change(self, cfg):
|
def on_well_change(self, cfg):
|
||||||
"""
|
"""
|
||||||
@@ -113,9 +116,11 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
return
|
return
|
||||||
|
|
||||||
params = self.DEFAULT_TRACKER_CONFIG if not cfg else cfg.to_params_dict()
|
params = self.DEFAULT_TRACKER_CONFIG if not cfg else cfg.to_params_dict()
|
||||||
|
|
||||||
self._params = ExperimentParams(params)
|
self._params = ExperimentParams(params)
|
||||||
self._metrics = self._params.build_metrics()
|
#self._metrics = self._params.build_metrics()
|
||||||
|
|
||||||
|
self._metrics = [self._params.build_metrics() for _ in range(self._params.planarian_count)]
|
||||||
|
|
||||||
self._tracker = PlanarianTracker(
|
self._tracker = PlanarianTracker(
|
||||||
tube_axis = self._params.tube_axis,
|
tube_axis = self._params.tube_axis,
|
||||||
min_area_px = self._params.min_area_px,
|
min_area_px = self._params.min_area_px,
|
||||||
@@ -265,6 +270,7 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
:return: Image traitée (JPEG ou PNG selon la stratégie)
|
:return: Image traitée (JPEG ou PNG selon la stratégie)
|
||||||
"""
|
"""
|
||||||
metrics = {"detected": False}
|
metrics = {"detected": False}
|
||||||
|
|
||||||
if self._circular_crop is not None:
|
if self._circular_crop is not None:
|
||||||
jpeg = self._circular_crop.process(jpeg_bytes)
|
jpeg = self._circular_crop.process(jpeg_bytes)
|
||||||
nparr = np.frombuffer(jpeg, np.uint8)
|
nparr = np.frombuffer(jpeg, np.uint8)
|
||||||
@@ -278,7 +284,7 @@ class VideoCaptureInterface(abc.ABC):
|
|||||||
annotated = self.align_detection.get('frame_annotated')
|
annotated = self.align_detection.get('frame_annotated')
|
||||||
frame = annotated if annotated is not None else frame
|
frame = annotated if annotated is not None else frame
|
||||||
##
|
##
|
||||||
# mode racking
|
# mode tracking
|
||||||
if self.use_tracking:
|
if self.use_tracking:
|
||||||
ts = datetime.now(timezone.utc).timestamp()
|
ts = datetime.now(timezone.utc).timestamp()
|
||||||
frame, metrics = self._tracker.process(frame, ts)
|
frame, metrics = self._tracker.process(frame, ts)
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import cv2
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from scipy.optimize import linear_sum_assignment
|
from scipy.optimize import linear_sum_assignment
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Nombre maximum de planaires suivis simultanément par tube
|
# Nombre maximum de planaires suivis simultanément par tube
|
||||||
MAX_PLANARIANS = 10
|
MAX_PLANARIANS = 10
|
||||||
@@ -386,7 +390,6 @@ class PlanarianTracker:
|
|||||||
for state in self._states:
|
for state in self._states:
|
||||||
if state.idx not in assigned_slots:
|
if state.idx not in assigned_slots:
|
||||||
state.mark_lost()
|
state.mark_lost()
|
||||||
|
|
||||||
return frame_out, results
|
return frame_out, results
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class ExperimentConfig(models.Model):
|
|||||||
def to_params_dict(self) -> dict:
|
def to_params_dict(self) -> dict:
|
||||||
"""Retourne un dict compatible avec ExperimentParams."""
|
"""Retourne un dict compatible avec ExperimentParams."""
|
||||||
return {
|
return {
|
||||||
"experiment": self.idendifier,
|
"experiment": self.identifier,
|
||||||
"well": self.well.name,
|
"well": self.well.name,
|
||||||
"px_per_mm": self.px_per_mm,
|
"px_per_mm": self.px_per_mm,
|
||||||
"fps": self.fps,
|
"fps": self.fps,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class ScannerConsumer(AsyncWebsocketConsumer):
|
|||||||
logger.info( f"==== Disconnect from {self.this_group}")
|
logger.info( f"==== Disconnect from {self.this_group}")
|
||||||
|
|
||||||
async def scanner_message(self, event):
|
async def scanner_message(self, event):
|
||||||
logger.info( f"===={event.get('type')}")
|
|
||||||
await self.send(text_data=json.dumps(event["text"]))
|
await self.send(text_data=json.dumps(event["text"]))
|
||||||
|
|
||||||
## Receive message from WebSocket
|
## Receive message from WebSocket
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Created on 20 avr. 2026
|
|||||||
|
|
||||||
@author: denis
|
@author: denis
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
@@ -18,10 +20,8 @@ from planarian.models import ExperimentConfig
|
|||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WellIterator:
|
class WellIterator:
|
||||||
"""Itérateur personnalisé pour naviguer dans les Wells"""
|
"""Itérateur personnalisé pour naviguer dans les Wells"""
|
||||||
|
|
||||||
@@ -76,9 +76,12 @@ class WellIterator:
|
|||||||
class MultiWellManager:
|
class MultiWellManager:
|
||||||
|
|
||||||
def __init__(self, process):
|
def __init__(self, process):
|
||||||
|
|
||||||
self.process = 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()
|
self.stop_playing = Event()
|
||||||
self.well_iterator = None
|
self.well_iterator = None
|
||||||
self.multiwel = None
|
self.multiwel = None
|
||||||
@@ -86,7 +89,8 @@ class MultiWellManager:
|
|||||||
self.set_multiwell()
|
self.set_multiwell()
|
||||||
self.scan_thread = None
|
self.scan_thread = None
|
||||||
self.test_thread = None
|
self.test_thread = None
|
||||||
self.tracker_config = dict(
|
|
||||||
|
self.tracker_config = dict(
|
||||||
tube_axis = settings.TRACKER_TUBE_AXIS,
|
tube_axis = settings.TRACKER_TUBE_AXIS,
|
||||||
min_area_px = self.process.conf.min_area_px,
|
min_area_px = self.process.conf.min_area_px,
|
||||||
max_area_ratio = self.process.conf.max_area_ratio,
|
max_area_ratio = self.process.conf.max_area_ratio,
|
||||||
@@ -96,6 +100,18 @@ class MultiWellManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def set_tracker_config(self):
|
||||||
|
self.tracker_config = dict(
|
||||||
|
tube_axis = settings.TRACKER_TUBE_AXIS,
|
||||||
|
min_area_px = self.process.conf.min_area_px,
|
||||||
|
max_area_ratio = self.process.conf.max_area_ratio,
|
||||||
|
max_planarians = self.process.conf.max_planarians,
|
||||||
|
merge_kernel_size = self.process.conf.merge_kernel_size,
|
||||||
|
min_contour_dist_px = self.process.conf.min_contour_dist_px,
|
||||||
|
)
|
||||||
|
logger.info(f"Tracker config: {self.tracker_config}")
|
||||||
|
|
||||||
def set_default_values(self, feed=None, step=None, duration=None):
|
def set_default_values(self, feed=None, step=None, duration=None):
|
||||||
self._feed = feed or self.process.conf.calibration_default_feed
|
self._feed = feed or self.process.conf.calibration_default_feed
|
||||||
self._step = step or self.process.conf.calibration_default_step
|
self._step = step or self.process.conf.calibration_default_step
|
||||||
@@ -136,7 +152,8 @@ class MultiWellManager:
|
|||||||
well = well_position.well
|
well = well_position.well
|
||||||
multiwell = experiment.multiwell
|
multiwell = experiment.multiwell
|
||||||
if self.process.use_tracking:
|
if self.process.use_tracking:
|
||||||
# Paramètres d'une expérience PlanarianScanner
|
logger.info("# Paramètres d'une expérience PlanarianScanner")
|
||||||
|
|
||||||
cfg = ExperimentConfig.objects.get(experiment_id=experiment.id, well_id=well.id)
|
cfg = ExperimentConfig.objects.get(experiment_id=experiment.id, well_id=well.id)
|
||||||
# reset PlanarianTracker => on_well_change
|
# reset PlanarianTracker => on_well_change
|
||||||
self.process.cam.on_well_change(cfg)
|
self.process.cam.on_well_change(cfg)
|
||||||
@@ -151,7 +168,7 @@ class MultiWellManager:
|
|||||||
while not self.stop_playing.is_set():
|
while not self.stop_playing.is_set():
|
||||||
if time.monotonic() - start > multiwell.duration:
|
if time.monotonic() - start > multiwell.duration:
|
||||||
break
|
break
|
||||||
self.cnc_controller.wait_for(1.0)
|
self.cnc_controller.wait_for(0.1)
|
||||||
|
|
||||||
self.process.data.record = False
|
self.process.data.record = False
|
||||||
self.process.data.uuid = None
|
self.process.data.uuid = None
|
||||||
@@ -175,11 +192,13 @@ class MultiWellManager:
|
|||||||
|
|
||||||
## change file
|
## change file
|
||||||
if self.process.conf.capture_type == 'file':
|
if self.process.conf.capture_type == 'file':
|
||||||
vf = settings.MEDIA_ROOT / 'simulation' / f'{wl.name}.mp4'
|
vf = settings.MEDIA_ROOT / 'simulation' / f'{wl.well.name}.mp4'
|
||||||
if vf.exists():
|
if vf.exists():
|
||||||
self.process.cam._video_file = str(vf)
|
self.process.cam._video_file = str(vf)
|
||||||
self.process.cam._error_occured = True
|
self.process.cam._error_occured = True
|
||||||
|
|
||||||
|
logger.info(f"Simulating capture with file {vf}")
|
||||||
|
|
||||||
self._grid_scanning_capture(experiment, wl, simulate=simulate)
|
self._grid_scanning_capture(experiment, wl, simulate=simulate)
|
||||||
|
|
||||||
|
|
||||||
@@ -188,40 +207,41 @@ class MultiWellManager:
|
|||||||
|
|
||||||
|
|
||||||
def _start_scanning(self, session, experiments, simulate=False):
|
def _start_scanning(self, session, experiments, simulate=False):
|
||||||
self.process.cam._aligner.debug = False
|
try:
|
||||||
xynext = []
|
self.process.cam._aligner.debug = False
|
||||||
for obs in experiments:
|
xynext = []
|
||||||
xynext.append((obs.multiwell.xbase, obs.multiwell.ybase))
|
for obs in experiments:
|
||||||
xynext.append((0, 0))
|
xynext.append((obs.multiwell.xbase, obs.multiwell.ybase))
|
||||||
|
xynext.append((0, 0))
|
||||||
|
|
||||||
pos = 1
|
pos = 1
|
||||||
self.process.data.session = session.id
|
self.process.data.session = session.id
|
||||||
started = timezone.now()
|
started = timezone.now()
|
||||||
for obs in experiments:
|
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, simulate=simulate)
|
||||||
|
obs.finished = timezone.now()
|
||||||
|
obs.save()
|
||||||
|
|
||||||
|
session.finished = timezone.now()
|
||||||
if self.stop_playing.is_set():
|
if self.stop_playing.is_set():
|
||||||
break
|
msg = f"Session {session.name} abandonnée à {session.finished} après {session.finished - started} secondes."
|
||||||
obs.started = timezone.now()
|
else:
|
||||||
obs.save()
|
if not simulate:
|
||||||
xnext, ynext = xynext[pos]
|
session.active = False
|
||||||
pos +=1
|
if session.scanning_task:
|
||||||
self._grid_scanning(obs, xnext=xnext, ynext=ynext, simulate=simulate)
|
session.scanning_task.enabled = False
|
||||||
obs.finished = timezone.now()
|
session.save()
|
||||||
obs.save()
|
msg = f"Session {session.name} terminée à {session.finished} après {session.finished - started} secondes."
|
||||||
|
logger.info(msg)
|
||||||
session.finished = timezone.now()
|
self.process._send(scan_state=msg)
|
||||||
if self.stop_playing.is_set():
|
finally:
|
||||||
msg = f"Session {session.name} abandonnée à {session.finished} après {session.finished - started} secondes."
|
self.scan_thread = None
|
||||||
else:
|
|
||||||
if not simulate:
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def halt_scanning(self):
|
def halt_scanning(self):
|
||||||
@@ -229,17 +249,18 @@ class MultiWellManager:
|
|||||||
self.stop_playing.set()
|
self.stop_playing.set()
|
||||||
self.well_iterator.reset()
|
self.well_iterator.reset()
|
||||||
self.process.cam._aligner.debug = False
|
self.process.cam._aligner.debug = False
|
||||||
|
self.scan_thread = None
|
||||||
|
self.test_thread = None
|
||||||
|
|
||||||
|
|
||||||
def scanning(self, sid, simulate=False):
|
def scan_process(self, sid, simulate=False):
|
||||||
try:
|
if self.scan_thread:
|
||||||
if self.scan_thread:
|
return
|
||||||
return
|
session = models.Session.objects.get(pk=sid)
|
||||||
session = models.Session.objects.get(pk=sid)
|
experiments = models.SessionExperiment.experiment_by_session(sid)
|
||||||
experiments = models.SessionExperiment.experiment_by_session(sid)
|
|
||||||
self.scan_thread = Thread(target=self._start_scanning, args=(session, experiments, simulate, ), daemon=True).start()
|
self.scan_thread = Thread(target=self._start_scanning, args=(session, experiments, simulate, ), daemon=True)
|
||||||
except Exception as e:
|
self.scan_thread.start()
|
||||||
print("MultiWellManager::scan error", e)
|
|
||||||
|
|
||||||
|
|
||||||
def previous_well(self):
|
def previous_well(self):
|
||||||
@@ -316,22 +337,24 @@ class MultiWellManager:
|
|||||||
self.process._send(state='center', msg=msg)
|
self.process._send(state='center', msg=msg)
|
||||||
|
|
||||||
self.cnc_controller.wait_for(0.1)
|
self.cnc_controller.wait_for(0.1)
|
||||||
logger.info("Fin du centrage")
|
logger.info("Fin du test")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
logger.error("Error during scanning test", e)
|
||||||
|
finally:
|
||||||
|
self.test_thread = None
|
||||||
|
|
||||||
self.well_iterator.reset()
|
self.well_iterator.reset()
|
||||||
self.process.cam._aligner.debug = False
|
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")
|
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.cnc_controller.move_to(0, 0, feed=self.multiwell.feed*2)
|
||||||
self.test_thread = None
|
|
||||||
|
|
||||||
|
|
||||||
def scan_test(self, auto=False):
|
def scan_test(self, auto=False):
|
||||||
if self.test_thread:
|
if self.test_thread:
|
||||||
return
|
return
|
||||||
self.test_thread = Thread(target=self._scanning_test, args=(auto, ), daemon=True).start()
|
self.test_thread = Thread(target=self._scanning_test, args=(auto, ), daemon=True)
|
||||||
|
self.test_thread.start()
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from modules import reductstore, grbl, utils, planarian_metrics
|
|||||||
from modules.circular_crop import CircularCrop, CropStrategy
|
from modules.circular_crop import CircularCrop, CropStrategy
|
||||||
from .multiwell import MultiWellManager
|
from .multiwell import MultiWellManager
|
||||||
from .constants import ScannerConstants
|
from .constants import ScannerConstants
|
||||||
#from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -248,10 +248,12 @@ class ScannerProcess(Task):
|
|||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
|
|
||||||
def start_services(self):
|
def start_services(self):
|
||||||
Thread(target=self._listen_to_redis, daemon=True).start()
|
Thread(target=self._init_manager, daemon=True).start()
|
||||||
Thread(target=self._recording, daemon=True).start()
|
Thread(target=self._recording, daemon=True).start()
|
||||||
Thread(target=self._init_grbl, daemon=True).start()
|
Thread(target=self._init_grbl, daemon=True).start()
|
||||||
|
Thread(target=self._listen_to_redis, daemon=True).start()
|
||||||
self.cam.start()
|
self.cam.start()
|
||||||
|
|
||||||
logger.warning(f"Scanner services started ...")
|
logger.warning(f"Scanner services started ...")
|
||||||
|
|
||||||
def _send(self, **payload):
|
def _send(self, **payload):
|
||||||
@@ -267,20 +269,21 @@ class ScannerProcess(Task):
|
|||||||
self._send(**msg)
|
self._send(**msg)
|
||||||
|
|
||||||
|
|
||||||
def _store_metrics(self, uuid, metrics, ts):
|
def _store_metric(self, uuid, metrics, ts):
|
||||||
|
|
||||||
|
if not isinstance(metrics, list):
|
||||||
|
return
|
||||||
for r in metrics:
|
for r in metrics:
|
||||||
pid = r["planarian_id"]
|
pid = r["planarian_id"]
|
||||||
record = self.cam._metrics[pid].update(r, well_radius_mm=self.cam._params.well_radius_mm)
|
record = self.cam._metrics[pid].update(r, well_radius_mm=self.cam._params.well_radius_mm)
|
||||||
logger.warning(f"{record}")
|
|
||||||
|
|
||||||
async_to_sync(planarianDB.store_metrics)(
|
async_to_sync(planarianDB.store_metric)(
|
||||||
record,
|
record,
|
||||||
self.cam._params.experiment,
|
self.cam._params.experiment,
|
||||||
self.cam._params.well_name,
|
self.cam._params.well,
|
||||||
entry_name=uuid,
|
entry_name=uuid,
|
||||||
planarian=pid,
|
planarian=pid,
|
||||||
record_type='metrics',
|
record_type='metrics',
|
||||||
ts_us=ts,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _store_frame(self, uuid, frame, ts, frame_count):
|
def _store_frame(self, uuid, frame, ts, frame_count):
|
||||||
@@ -292,15 +295,14 @@ class ScannerProcess(Task):
|
|||||||
self.recordDB.write(uuid, frame, ts=ts, labels=labels)
|
self.recordDB.write(uuid, frame, ts=ts, labels=labels)
|
||||||
|
|
||||||
def _on_frame(self, jpeg_bytes: bytes, ts: datetime, metrics: dict, frame_count: int = 0) -> None:
|
def _on_frame(self, jpeg_bytes: bytes, ts: datetime, metrics: dict, frame_count: int = 0) -> None:
|
||||||
|
try:
|
||||||
if self.data.record:
|
if self.data.record:
|
||||||
self.record_queue.put((self.data.uuid, ts, jpeg_bytes, metrics, frame_count))
|
self.record_queue.put((self.data.uuid, ts, jpeg_bytes, metrics, frame_count))
|
||||||
if self.data.play:
|
if self.data.play:
|
||||||
try:
|
jpeg=base64.b64encode(jpeg_bytes).decode()
|
||||||
jpeg=base64.b64encode(jpeg_bytes).decode()
|
self._send(ts=ts.timestamp(), jpeg=jpeg, frame_count=frame_count)
|
||||||
self._send(ts=ts.timestamp(), jpeg=jpeg, frame_count=frame_count)
|
except Exception as e:
|
||||||
except Exception as e:
|
logger.error(e)
|
||||||
logger.error(f"----_on_frame: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _recording(self):
|
def _recording(self):
|
||||||
@@ -308,8 +310,11 @@ class ScannerProcess(Task):
|
|||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
(uuid, ts, frame, metrics, frame_count) = self.record_queue.get()
|
(uuid, ts, frame, metrics, frame_count) = self.record_queue.get()
|
||||||
self._store_metrics(uuid, metrics, ts)
|
if self.cam.use_tracking:
|
||||||
self._frame_store(uuid, frame, ts, frame_count)
|
self._store_metric(uuid, metrics, ts)
|
||||||
|
|
||||||
|
self._store_frame(uuid, frame, ts, frame_count)
|
||||||
|
|
||||||
self.record_queue.task_done()
|
self.record_queue.task_done()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'recorder: {e}')
|
logger.error(f'recorder: {e}')
|
||||||
@@ -324,14 +329,18 @@ class ScannerProcess(Task):
|
|||||||
self.grbl.wait_for(2.0)
|
self.grbl.wait_for(2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def _init_manager(self):
|
||||||
|
logger.info(f"==== Start MultiWellManager!")
|
||||||
|
self.manager = MultiWellManager(process=self)
|
||||||
|
self.manager.set_calib_debug(False)
|
||||||
|
Event().wait(2.0)
|
||||||
|
|
||||||
|
|
||||||
def _listen_to_redis(self):
|
def _listen_to_redis(self):
|
||||||
try:
|
try:
|
||||||
logger.info(f"==== Scanner {self.group}: listen via redisDB")
|
logger.info(f"==== Scanner {self.group}: listen via redisDB")
|
||||||
pubsub = redisDB.pubsub()
|
pubsub = redisDB.pubsub()
|
||||||
pubsub.subscribe(self.group)
|
pubsub.subscribe(self.group)
|
||||||
#self._init_grbl()
|
|
||||||
self.manager = MultiWellManager(process=self)
|
|
||||||
self.manager.set_calib_debug(False)
|
|
||||||
|
|
||||||
for message in pubsub.listen():
|
for message in pubsub.listen():
|
||||||
try:
|
try:
|
||||||
@@ -356,12 +365,22 @@ class ScannerProcess(Task):
|
|||||||
elif topic == 'scan' or topic == 'simulate':
|
elif topic == 'scan' or topic == 'simulate':
|
||||||
logger.info(f"==== Scan {cmd}")
|
logger.info(f"==== Scan {cmd}")
|
||||||
sid = cmd.get("session", '0')
|
sid = cmd.get("session", '0')
|
||||||
|
|
||||||
if sid == "0":
|
if sid == "0":
|
||||||
self._send(state='error', msg=str(_('La session est nulle!...')))
|
self._send(state='error', msg=str(_('La session est nulle!...')))
|
||||||
else:
|
else:
|
||||||
self.cam._active_median = False
|
|
||||||
self.manager.scanning(sid, simulate=(topic=='simulate'))
|
try:
|
||||||
self._send(state=topic, msg=str(_('Balayage démarré...')))
|
self.cam._active_median = False
|
||||||
|
simulate = (topic=='simulate')
|
||||||
|
self.manager.scan_process(sid, simulate)
|
||||||
|
|
||||||
|
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...')))
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
elif cmd["type"]=="calibrate":
|
elif cmd["type"]=="calibrate":
|
||||||
topic = cmd.get("topic")
|
topic = cmd.get("topic")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class ScannerManager {
|
|||||||
this.area_px = options.area_px;
|
this.area_px = options.area_px;
|
||||||
this.frame_count = options.frame_count;
|
this.frame_count = options.frame_count;
|
||||||
this.scan_state = options.scan_state;
|
this.scan_state = options.scan_state;
|
||||||
|
this.sim_bt = options.simulate;
|
||||||
}
|
}
|
||||||
|
|
||||||
init_controls() {
|
init_controls() {
|
||||||
@@ -30,6 +31,7 @@ class ScannerManager {
|
|||||||
this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); });
|
this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); });
|
||||||
this.scan_bt.addEventListener('click', (e) => { this.scan(); });
|
this.scan_bt.addEventListener('click', (e) => { this.scan(); });
|
||||||
this.halt_bt.addEventListener('click', (e) => { this.halt(); });
|
this.halt_bt.addEventListener('click', (e) => { this.halt(); });
|
||||||
|
this.sim_bt.addEventListener('click', (e) => { this.simulate(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSocket(socket) {
|
registerSocket(socket) {
|
||||||
@@ -58,6 +60,7 @@ class ScannerManager {
|
|||||||
|
|
||||||
init() { this.axes = 0; this.cropping = 1; this._send({ type: 'scanner', topic: "init", }); }
|
init() { this.axes = 0; this.cropping = 1; this._send({ type: 'scanner', topic: "init", }); }
|
||||||
scan() { this._send({ type: 'scanner', topic: "scan", session: this.session.value ? this.session.value: "0" }); }
|
scan() { this._send({ type: 'scanner', topic: "scan", session: this.session.value ? this.session.value: "0" }); }
|
||||||
|
simulate() { this._send({ type: 'scanner', topic: "simulate", session: this.session.value ? this.session.value: "0" }); }
|
||||||
halt() { this._send({ type: 'calibrate', topic: "halt" }); }
|
halt() { this._send({ type: 'calibrate', topic: "halt" }); }
|
||||||
|
|
||||||
_send(message) { this.socket.send(message); }
|
_send(message) { this.socket.send(message); }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# tasks.py
|
# tasks.py
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
from celery import shared_task, group, chord, chain
|
from celery import shared_task, group, chord, chain
|
||||||
from celery.utils.log import get_task_logger
|
from celery.utils.log import get_task_logger
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -215,3 +216,63 @@ def on_scanning_done(result: dict, session_id: str):
|
|||||||
|
|
||||||
logger.info("==== Scanning done: session %s", session_id)
|
logger.info("==== Scanning done: session %s", session_id)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def http_request(req_type='json_post', url=None, user=None, passwd=None, headers={}, **kwargs):
|
||||||
|
|
||||||
|
def json_post():
|
||||||
|
return requests.post(url, json=kwargs, headers=headers, auth=(user, passwd), timeout=3)
|
||||||
|
|
||||||
|
def post():
|
||||||
|
return requests.post(url, params=kwargs, headers=headers, auth=(user, passwd), timeout=3)
|
||||||
|
|
||||||
|
def get():
|
||||||
|
return requests.get(url, params=kwargs, headers=headers, auth=(user, passwd), timeout=3)
|
||||||
|
|
||||||
|
request = {
|
||||||
|
'json_post': json_post,
|
||||||
|
'post': post,
|
||||||
|
'get':get
|
||||||
|
}
|
||||||
|
retries = 3
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
response = request[req_type]()
|
||||||
|
response.raise_for_status()
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except ValueError:
|
||||||
|
data = response.text
|
||||||
|
|
||||||
|
resp = {"success": True, "data": data}
|
||||||
|
logger.info(data)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.error(f"{attempt + 1}/{retries}: Délai d'attente dépassé")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
logger.error(f"{attempt + 1}/{retries}: Erreur de connexion")
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur inattendue: {str(e)}")
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def supervisor_restart_service(params):
|
||||||
|
"""
|
||||||
|
Redémarre un service.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.warning("==== Supervisor scanner restart all services")
|
||||||
|
http_request(
|
||||||
|
req_type='get',
|
||||||
|
url=f'http://localhost:9001/{params}',
|
||||||
|
user='root',
|
||||||
|
passwd='toor',
|
||||||
|
headers={}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"supervisor_restart_all_services error {e}")
|
||||||
|
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-row w3-row-padding">
|
<div class="w3-row w3-row-padding">
|
||||||
<div class="w3-twothird">
|
<div class="w3-twothird w3-center">
|
||||||
<button id="_scan" class="w3-button w3-light-blue w3-round-large w3-margin-top w3-block">
|
<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>
|
{% trans 'Lancer' %}<br>{% trans 'le balayage' %}<br><i class="fa-solid fa-circle-check w3-text-blue w3-xlarge"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-third">
|
<div class="w3-third w3-center">
|
||||||
<button id="_scan" class="w3-button w3-dark-xlight w3-round-large w3-margin-top w3-block">
|
<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>
|
{% trans 'Simuler' %}<br>{% trans 'le balayage' %}<br><i class="fa-solid fa-triangle-exclamation w3-text-orange w3-xlarge"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ urlpatterns = [
|
|||||||
path('replay/', views.replay_view, name='replay'),
|
path('replay/', views.replay_view, name='replay'),
|
||||||
path('api/stats/', views.stats_view, name='api_stats'),
|
path('api/stats/', views.stats_view, name='api_stats'),
|
||||||
path('api/video/', views.download_api, name='download_api'),
|
path('api/video/', views.download_api, name='download_api'),
|
||||||
|
path('api/restart/', views.restart_all, name='restart_all'),
|
||||||
path('api/export/', views.export_api, name='export_api'),
|
path('api/export/', views.export_api, name='export_api'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
import base64, json
|
import base64, json
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, redirect
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_GET, require_POST
|
from django.views.decorators.http import require_GET, require_POST
|
||||||
@@ -12,7 +12,7 @@ from reduct.time import unix_timestamp_to_iso
|
|||||||
from modules.system_stats import get_cached_stats, start_background_updater
|
from modules.system_stats import get_cached_stats, start_background_updater
|
||||||
from modules import reductstore
|
from modules import reductstore
|
||||||
|
|
||||||
from .tasks import download_video, export_all_images, export_all_videos
|
from .tasks import download_video, export_all_images, export_all_videos, supervisor_restart_service
|
||||||
from .process import CameraRecordManager, cameraDB
|
from .process import CameraRecordManager, cameraDB
|
||||||
from . import models
|
from . import models
|
||||||
from .constants import ScannerConstants
|
from .constants import ScannerConstants
|
||||||
@@ -157,6 +157,17 @@ def images_view(request):
|
|||||||
|
|
||||||
|
|
||||||
## replay
|
## replay
|
||||||
|
@require_GET
|
||||||
|
@csrf_exempt
|
||||||
|
def restart_all(request):
|
||||||
|
#http://scanner.local:9001/index.html?processname=test_tube%3Aservices&action=restart
|
||||||
|
#http://scanner.local:9001/index.html?processname=test_tube%3AwebUI&action=start
|
||||||
|
#http://scanner.local:9001/index.html?action=restartall
|
||||||
|
#supervisor_restart_service('index.html?action=restartall')
|
||||||
|
supervisor_restart_service('index.html?processname=test_tube%3Aservices&action=restart')
|
||||||
|
return JsonResponse({"state": True})
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def download_api(request):
|
def download_api(request):
|
||||||
|
|||||||
Reference in New Issue
Block a user