Commit 3997b8ca authored by vertighel's avatar vertighel
Browse files

atik2

parent 0396b0e0
Loading
Loading
Loading
Loading
Loading
+54 −10
Original line number Diff line number Diff line
@@ -243,16 +243,60 @@ during the devices phase (no hardware needed); CCD-TEMP is still open.
  atik.py` wrapper (which happened with `self._looping` presumably
  true) — here, single-threaded, no loop, state is `-1` from the start
  and stays that way. `test_atik.py` was extended with a `wait_idle()`
  poll (up to 3s) to rule out a simple post-connect settle-time race
  before concluding it's a persistent error — not yet re-tested after
  this change. If it's still `-1` even after the poll, this points to
  a real hardware/driver/USB fault on this specific camera or its
  connection on `fork`, independent of and possibly underlying several
  other open Atik items above (the `CCD-TEMP` sentinel corruption, and
  perhaps the subframe/cooling behavior too) — needs `dmesg`/`lsusb`
  around the connect, and checking the SDK's Linux driver/udev
  installation (see the SDK guide's Section 4.2) hasn't drifted, not a
  code fix.
  poll (up to 3s, later manually extended to 30s while testing).
  **Root cause found on `fork` (2026-07-22), and it's an environment
  problem, not hardware and not a `noctua` code bug: the wrong Python
  interpreter.** Chased through several red herrings before landing on
  it, each ruled out in turn:
  - *USB extender*: `lsusb` showed two Cypress (`04b4:...`) devices —
    `04b4:6506 "CY4603"` (a genuine 4-port USB hub, confirmed part of
    the extender, disappears when it's unplugged) and `04b4:df28` (the
    Atik camera itself, with zero Mfr/Product/SerialNumber descriptor
    strings — looked like an unprogrammed Cypress FX2/FX3 chip stuck in
    bootloader mode, unlike the SBIG STL sharing the same hub, which
    correctly shows `"SBIG Astronomy Camera (with firmware)"`). It
    worked once right after power-cycling camera+extender together,
    then stopped — looked exactly like a marginal firmware-upload link.
    **Ruled out**: see below, same device/topology still fails or
    succeeds purely based on which Python runs the script.
  - *Session/permissions (`uaccess`/logind ACL)*: `sudo` from a fresh
    plain SSH session (no tmux) made it work reliably — looked like a
    privilege issue. `ls -l`/`getfacl` on the device node showed plain
    `crw-rw-rw-` (0666) with no extra ACL, so file permissions were
    never actually the blocker. **Ruled out**: `strace -f -e
    trace=ioctl,openat` comparing a failing (no sudo) and succeeding
    (sudo) run showed the *identical* set of ioctls
    (`USBDEVFS_SUBMITURB`/`REAPURBNDELAY`/etc.), no `EPERM`/`EACCES`
    anywhere — no privileged operation was ever attempted or denied.
  - **Actual cause, found in the same strace comparison**: the failing
    run's very first `openat` was
    `/home/antola/.pyenv/shims/python3` (pyenv's shim); the succeeding
    (`sudo`) run's was the system Python 3.10
    (`/usr/lib/python3.10/...`, no `pyenv`/`pyvenv.cfg` anywhere). `sudo`
    resets `PATH` (`secure_path` in `/etc/sudoers`), which incidentally
    bypasses `~/.pyenv/shims` — that's *why* `sudo` "fixed" it, nothing
    to do with privilege. **Confirmed directly** by the user: running
    `test_atik.py` with `/usr/bin/env python3` → system Python works
    every time, in tmux *and* plain SSH; running it explicitly with
    pyenv's Python (currently pinned to **3.14**, very new) fails every
    time, in both session types. Root cause is Python 3.14 (or
    something specific to this pyenv build of it) interacting
    unreliably with the Artemis SDK's shared library over `ctypes` +
    `usbfs` — the exact mechanism (GIL/threading/signal handling
    difference between the two Python builds?) wasn't pinned down
    further; not needed once the interpreter swap reliably fixes it.
  **Not a code fix, not a hardware fix** — none of the Python-level
  fixes above (subframe-while-looping guard, setpoint telemetry) were
  wrong, they remain correct; the extender and USB permissions were
  both innocent. **Action needed**: verify what Python interpreter
  `noctua-api` actually runs under in production on `fork` — if it's
  also resolving to the pyenv 3.14 shim, that alone could explain
  *every* previously-tracked Atik flakiness in this document (`CCD-TEMP`
  sentinel corruption, the subframe full-frame bug, the cooling
  oddities) as one root cause rather than several unrelated hardware
  bugs. Pin `noctua-api`'s entry point to the system Python (or
  whichever interpreter is confirmed reliable) if so — not done yet,
  waiting on the user's check.

## Regressions found on `fork` post-refactor (2026-07-22)

+96 −7
Original line number Diff line number Diff line
@@ -88,14 +88,98 @@ class Camera(BaseDevice):

        try:
            self._lib = ctypes.CDLL("/usr/lib/libatikcameras.so")
            self._lib.ArtemisConnect.restype = ctypes.c_void_p
            self._lib.ArtemisImageBuffer.restype = ctypes.c_void_p
            self._lib.ArtemisExposureTimeRemaining.restype = ctypes.c_float
            self._lib.ArtemisLastExposureDuration.restype = ctypes.c_float
            self._lib.ArtemisLastStartTime.restype = ctypes.c_char_p
            self._declare_signatures()
        except Exception as e:
            log.error(f"Atik SDK load failed: {e}")

    def _declare_signatures(self):
        """Declare argtypes/restype for every Artemis SDK function called
        from this file.

        Without these, ctypes falls back to its own implicit argument
        marshaling, which isn't part of ctypes' stable ABI and was found
        to behave differently across Python versions — the same calls
        that worked reliably under Python 3.10 returned corrupt/failed
        results under 3.14 (see PLAN.md). Types transcribed from the
        vendor's own ctypesgen-generated Python SDK
        (external-software/AtikPythonSDK_v1.5.1), which declares all of
        these explicitly and doesn't show the same instability.
        """

        h = ctypes.c_void_p
        i = ctypes.c_int
        p_i = ctypes.POINTER(ctypes.c_int)
        lib = self._lib

        lib.ArtemisDeviceCount.argtypes = []
        lib.ArtemisDeviceCount.restype  = i

        lib.ArtemisConnect.argtypes = [i]
        lib.ArtemisConnect.restype  = h

        lib.ArtemisDisconnect.argtypes = [h]
        lib.ArtemisDisconnect.restype  = i

        lib.ArtemisIsConnected.argtypes = [h]
        lib.ArtemisIsConnected.restype  = i

        lib.ArtemisProperties.argtypes = [h, ctypes.POINTER(ArtemisProperties)]
        lib.ArtemisProperties.restype  = i

        lib.ArtemisCoolingInfo.argtypes = [h, p_i, p_i, p_i, p_i, p_i]
        lib.ArtemisCoolingInfo.restype  = i

        lib.ArtemisSetCooling.argtypes = [h, i]
        lib.ArtemisSetCooling.restype  = i

        lib.ArtemisCoolerWarmUp.argtypes = [h]
        lib.ArtemisCoolerWarmUp.restype  = i

        lib.ArtemisTemperatureSensorInfo.argtypes = [h, i, p_i]
        lib.ArtemisTemperatureSensorInfo.restype  = i

        lib.ArtemisAbortExposure.argtypes = [h]
        lib.ArtemisAbortExposure.restype  = i

        lib.ArtemisSetDarkMode.argtypes = [h, ctypes.c_bool]
        lib.ArtemisSetDarkMode.restype  = i

        lib.ArtemisStartExposure.argtypes = [h, ctypes.c_float]
        lib.ArtemisStartExposure.restype  = i

        lib.ArtemisImageReady.argtypes = [h]
        lib.ArtemisImageReady.restype  = i

        lib.ArtemisGetImageData.argtypes = [h, p_i, p_i, p_i, p_i, p_i, p_i]
        lib.ArtemisGetImageData.restype  = i

        lib.ArtemisImageBuffer.argtypes = [h]
        lib.ArtemisImageBuffer.restype  = ctypes.c_void_p

        lib.ArtemisLastExposureDuration.argtypes = [h]
        lib.ArtemisLastExposureDuration.restype  = ctypes.c_float

        lib.ArtemisLastStartTime.argtypes = [h]
        lib.ArtemisLastStartTime.restype  = ctypes.c_char_p

        lib.ArtemisLastStartTimeMilliseconds.argtypes = [h]
        lib.ArtemisLastStartTimeMilliseconds.restype  = i

        lib.ArtemisExposureTimeRemaining.argtypes = [h]
        lib.ArtemisExposureTimeRemaining.restype  = ctypes.c_float

        lib.ArtemisSubframe.argtypes = [h, i, i, i, i]
        lib.ArtemisSubframe.restype  = i

        lib.ArtemisCameraState.argtypes = [h]
        lib.ArtemisCameraState.restype  = i

        lib.ArtemisGetBin.argtypes = [h, p_i, p_i]
        lib.ArtemisGetBin.restype  = i

        lib.ArtemisBin.argtypes = [h, i, i]
        lib.ArtemisBin.restype  = i

    def _check_connection(self):
        """Internal method to manage the persistent camera handle.

@@ -328,10 +412,15 @@ class Camera(BaseDevice):
        from ..config.constants import frame_type as _frame_type
        Path(filepath).parent.mkdir(parents=True, exist_ok=True)

        
        size = w.value * h_img.value
        buffer = (ctypes.c_uint16 * size).from_address(buf_ptr)
        data = np.frombuffer(buffer, dtype=np.uint16).reshape(h_img.value, w.value)
        # np.array(...): an owned copy, not a read-only view straight over
        # the SDK's own buffer — without it astropy silently wrote no
        # BZERO/BSCALE for this uint16 data (fixed once already on main in
        # e662296, lost when this file was rewritten for the devices phase
        # without that branch's fix; see PLAN.md).
        raw = np.frombuffer(buffer, dtype=np.uint16).reshape(h_img.value, w.value)
        data = np.array(raw)

        hdu = fits.PrimaryHDU(data)
        hdr = hdu.header
+781 −0

File added.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@ dependencies = [
    # "Fourth" party :)
    "PIPython", # for PI - Physik Instrumente controllers
    "vmbpy @ file:lib/vmbpy-1.2.1-py3-none-manylinux_2_27_x86_64.whl",
    "AtikSDK @ file:lib/Atik_Python_SDK-1.5.1-py3-none-any.whl", # used by devices/atik2.py, still requires libatikcameras.so on the system
]

[project.urls]