Loading noctua/devices/mako.py +40 −4 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ import numpy as np # Third-party modules from astropy.io import fits from vmbpy import VmbSystem from vmbpy import FrameStatus, VmbSystem # Custom modules from .basedevice import BaseDevice Loading @@ -30,12 +30,33 @@ class Mako(BaseDevice): The camera ID is the IP address on the GigE network. """ # Conservative default: caps the GVSP stream well below a 100Mb/s link # (~12.5MB/s raw). Weak/old links on the observatory LAN (e.g. an # aging PC's Fast Ethernet port) otherwise get flooded at the camera's # native GigE rate and silently drop packets, producing frames with # missing row bands. Frame rate ends up throughput_limit / frame_size, # which is plenty for the 1-10 fps this driver actually needs. throughput_limit_bps = 8_000_000 def __init__(self, url): super().__init__(url) self.id = url self.vmb = VmbSystem.get_instance() self._cam = None def _set_throughput_limit(self, cam): """Cap the camera's GVSP output rate so it never exceeds what the network path to the client can actually drain.""" limit = self.throughput_limit_bps try: cam.get_feature_by_name('DeviceLinkThroughputLimitMode').set('On') cam.get_feature_by_name('DeviceLinkThroughputLimit').set(limit) except Exception: try: cam.get_feature_by_name('StreamBytesPerSecond').set(limit) except Exception as e: log.warning(f"Mako: could not set throughput limit: {e}") def _check_connection(self): """Open and cache the VmbSystem + Camera context.""" if self._cam is None: Loading @@ -50,6 +71,7 @@ class Mako(BaseDevice): pass except Exception: pass self._set_throughput_limit(self._cam) except Exception as e: self._cam = None msg = f"Mako connection failed: {e}" Loading Loading @@ -165,13 +187,27 @@ class Guider(Mako): except Exception: pass max_acquire_retries = 3 def _acquire(self, cam, exptime_s=None): """Call get_frame() with a timeout derived from the exposure time.""" """Call get_frame() with a timeout derived from the exposure time. Retries on incomplete frames (dropped GVSP packets on a slow/lossy link) rather than silently returning a partially-filled buffer. """ if exptime_s is None: exptime_s = self.loop_exposure timeout_ms = int(exptime_s * 1000) + 5000 last_status = None for attempt in range(self.max_acquire_retries): frame = cam.get_frame(timeout_ms=timeout_ms) last_status = frame.get_status() if last_status == FrameStatus.Complete: return np.squeeze(frame.as_numpy_ndarray().copy()) log.warning(f"Mako: incomplete frame ({last_status}), " f"retry {attempt + 1}/{self.max_acquire_retries}") raise IOError(f"Mako: {self.max_acquire_retries} consecutive incomplete frames " f"(last status: {last_status})") # --- Acquisition --- Loading prova_teccam.py +43 −4 Original line number Diff line number Diff line Loading @@ -28,8 +28,17 @@ HERE = Path(__file__).parent class TecCam: """Wrapper minimale per acquisizione singola da Mako GigE.""" def __init__(self, ip): # Limita il rate GVSP della camera sotto la capacita' reale del link piu' # lento (es. 100Mb/s ~ 12.5MB/s su un vecchio PC). Senza questo limite la # camera trasmette al suo rate nativo GigE, il link piu' lento non riesce # a smaltirlo e si perdono pacchetti (bande di righe mancanti in fondo # all'immagine). A 8MB/s il frame rate risultante e' comunque piu' che # sufficiente per 1-10 fps. max_acquire_retries = 3 def __init__(self, ip, maxrate=8_000_000): self.ip = ip self.maxrate = maxrate self.array = None # numpy (H, W), aggiornato da acquire() def acquire(self, exptime_s, gain=None, binning=1): Loading @@ -42,19 +51,38 @@ class TecCam: cam = vmb.get_camera_by_id(self.ip) with cam: self._adjust_packet_size(cam) self._set_throughput_limit(cam) self._set_exposure(cam, exptime_s) if gain is not None: self._set_gain(cam, float(gain)) self._set_binning(cam, int(binning)) frame = cam.get_frame(timeout_ms=timeout_ms) raw = frame.as_numpy_ndarray() raw = self._acquire_frame(cam, timeout_ms) self.array = np.squeeze(raw.copy()) print(f"Acquisita: shape={self.array.shape} dtype={self.array.dtype} " f"min={self.array.min()} max={self.array.max()}") return self.array def _acquire_frame(self, cam, timeout_ms): """get_frame() con verifica di completezza e retry. Un frame Incomplete (pacchetti GVSP persi) non viene mai accettato silenziosamente: si ritenta fino a max_acquire_retries volte. """ import vmbpy last_status = None for attempt in range(self.max_acquire_retries): frame = cam.get_frame(timeout_ms=timeout_ms) last_status = frame.get_status() if last_status == vmbpy.FrameStatus.Complete: return frame.as_numpy_ndarray() print(f" [warn] frame incompleto ({last_status}), " f"retry {attempt + 1}/{self.max_acquire_retries}") raise IOError(f"{self.max_acquire_retries} frame incompleti consecutivi " f"(ultimo status: {last_status})") # -- feature helpers -- def _try_set(self, cam, name, value): Loading @@ -74,6 +102,15 @@ class TecCam: except Exception: pass def _set_throughput_limit(self, cam): """Limita il rate di trasmissione GVSP della camera (bytes/s).""" try: cam.get_feature_by_name('DeviceLinkThroughputLimitMode').set('On') cam.get_feature_by_name('DeviceLinkThroughputLimit').set(self.maxrate) except Exception: if not self._try_set(cam, 'StreamBytesPerSecond', self.maxrate): print(" [warn] throughput limit non impostabile su questa camera") def _set_exposure(self, cam, exptime_s): self._try_set(cam, 'ExposureAuto', 'Off') us = exptime_s * 1e6 Loading Loading @@ -154,9 +191,11 @@ def main(): parser.add_argument('--fits', action='store_true', help='Salva anche in FITS') parser.add_argument('--vmin', type=float, default=None, help='Min per PNG') parser.add_argument('--vmax', type=float, default=None, help='Max per PNG') parser.add_argument('--maxrate',type=int, default=8_000_000, help='Limite banda camera in byte/s (default 8000000, adatto a link a 100Mb/s)') args = parser.parse_args() cam = TecCam(args.ip) cam = TecCam(args.ip, maxrate=args.maxrate) cam.acquire(args.exptime, gain=args.gain, binning=args.binning) cam.save_png(vmin=args.vmin, vmax=args.vmax) if args.fits: Loading Loading
noctua/devices/mako.py +40 −4 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ import numpy as np # Third-party modules from astropy.io import fits from vmbpy import VmbSystem from vmbpy import FrameStatus, VmbSystem # Custom modules from .basedevice import BaseDevice Loading @@ -30,12 +30,33 @@ class Mako(BaseDevice): The camera ID is the IP address on the GigE network. """ # Conservative default: caps the GVSP stream well below a 100Mb/s link # (~12.5MB/s raw). Weak/old links on the observatory LAN (e.g. an # aging PC's Fast Ethernet port) otherwise get flooded at the camera's # native GigE rate and silently drop packets, producing frames with # missing row bands. Frame rate ends up throughput_limit / frame_size, # which is plenty for the 1-10 fps this driver actually needs. throughput_limit_bps = 8_000_000 def __init__(self, url): super().__init__(url) self.id = url self.vmb = VmbSystem.get_instance() self._cam = None def _set_throughput_limit(self, cam): """Cap the camera's GVSP output rate so it never exceeds what the network path to the client can actually drain.""" limit = self.throughput_limit_bps try: cam.get_feature_by_name('DeviceLinkThroughputLimitMode').set('On') cam.get_feature_by_name('DeviceLinkThroughputLimit').set(limit) except Exception: try: cam.get_feature_by_name('StreamBytesPerSecond').set(limit) except Exception as e: log.warning(f"Mako: could not set throughput limit: {e}") def _check_connection(self): """Open and cache the VmbSystem + Camera context.""" if self._cam is None: Loading @@ -50,6 +71,7 @@ class Mako(BaseDevice): pass except Exception: pass self._set_throughput_limit(self._cam) except Exception as e: self._cam = None msg = f"Mako connection failed: {e}" Loading Loading @@ -165,13 +187,27 @@ class Guider(Mako): except Exception: pass max_acquire_retries = 3 def _acquire(self, cam, exptime_s=None): """Call get_frame() with a timeout derived from the exposure time.""" """Call get_frame() with a timeout derived from the exposure time. Retries on incomplete frames (dropped GVSP packets on a slow/lossy link) rather than silently returning a partially-filled buffer. """ if exptime_s is None: exptime_s = self.loop_exposure timeout_ms = int(exptime_s * 1000) + 5000 last_status = None for attempt in range(self.max_acquire_retries): frame = cam.get_frame(timeout_ms=timeout_ms) last_status = frame.get_status() if last_status == FrameStatus.Complete: return np.squeeze(frame.as_numpy_ndarray().copy()) log.warning(f"Mako: incomplete frame ({last_status}), " f"retry {attempt + 1}/{self.max_acquire_retries}") raise IOError(f"Mako: {self.max_acquire_retries} consecutive incomplete frames " f"(last status: {last_status})") # --- Acquisition --- Loading
prova_teccam.py +43 −4 Original line number Diff line number Diff line Loading @@ -28,8 +28,17 @@ HERE = Path(__file__).parent class TecCam: """Wrapper minimale per acquisizione singola da Mako GigE.""" def __init__(self, ip): # Limita il rate GVSP della camera sotto la capacita' reale del link piu' # lento (es. 100Mb/s ~ 12.5MB/s su un vecchio PC). Senza questo limite la # camera trasmette al suo rate nativo GigE, il link piu' lento non riesce # a smaltirlo e si perdono pacchetti (bande di righe mancanti in fondo # all'immagine). A 8MB/s il frame rate risultante e' comunque piu' che # sufficiente per 1-10 fps. max_acquire_retries = 3 def __init__(self, ip, maxrate=8_000_000): self.ip = ip self.maxrate = maxrate self.array = None # numpy (H, W), aggiornato da acquire() def acquire(self, exptime_s, gain=None, binning=1): Loading @@ -42,19 +51,38 @@ class TecCam: cam = vmb.get_camera_by_id(self.ip) with cam: self._adjust_packet_size(cam) self._set_throughput_limit(cam) self._set_exposure(cam, exptime_s) if gain is not None: self._set_gain(cam, float(gain)) self._set_binning(cam, int(binning)) frame = cam.get_frame(timeout_ms=timeout_ms) raw = frame.as_numpy_ndarray() raw = self._acquire_frame(cam, timeout_ms) self.array = np.squeeze(raw.copy()) print(f"Acquisita: shape={self.array.shape} dtype={self.array.dtype} " f"min={self.array.min()} max={self.array.max()}") return self.array def _acquire_frame(self, cam, timeout_ms): """get_frame() con verifica di completezza e retry. Un frame Incomplete (pacchetti GVSP persi) non viene mai accettato silenziosamente: si ritenta fino a max_acquire_retries volte. """ import vmbpy last_status = None for attempt in range(self.max_acquire_retries): frame = cam.get_frame(timeout_ms=timeout_ms) last_status = frame.get_status() if last_status == vmbpy.FrameStatus.Complete: return frame.as_numpy_ndarray() print(f" [warn] frame incompleto ({last_status}), " f"retry {attempt + 1}/{self.max_acquire_retries}") raise IOError(f"{self.max_acquire_retries} frame incompleti consecutivi " f"(ultimo status: {last_status})") # -- feature helpers -- def _try_set(self, cam, name, value): Loading @@ -74,6 +102,15 @@ class TecCam: except Exception: pass def _set_throughput_limit(self, cam): """Limita il rate di trasmissione GVSP della camera (bytes/s).""" try: cam.get_feature_by_name('DeviceLinkThroughputLimitMode').set('On') cam.get_feature_by_name('DeviceLinkThroughputLimit').set(self.maxrate) except Exception: if not self._try_set(cam, 'StreamBytesPerSecond', self.maxrate): print(" [warn] throughput limit non impostabile su questa camera") def _set_exposure(self, cam, exptime_s): self._try_set(cam, 'ExposureAuto', 'Off') us = exptime_s * 1e6 Loading Loading @@ -154,9 +191,11 @@ def main(): parser.add_argument('--fits', action='store_true', help='Salva anche in FITS') parser.add_argument('--vmin', type=float, default=None, help='Min per PNG') parser.add_argument('--vmax', type=float, default=None, help='Max per PNG') parser.add_argument('--maxrate',type=int, default=8_000_000, help='Limite banda camera in byte/s (default 8000000, adatto a link a 100Mb/s)') args = parser.parse_args() cam = TecCam(args.ip) cam = TecCam(args.ip, maxrate=args.maxrate) cam.acquire(args.exptime, gain=args.gain, binning=args.binning) cam.save_png(vmin=args.vmin, vmax=args.vmax) if args.fits: Loading