Skip to content

Commit 8fe3644

Browse files
authored
v1.0.1
version 1.0.1
2 parents 98ada8f + 153bab0 commit 8fe3644

File tree

9 files changed

+403
-34
lines changed

9 files changed

+403
-34
lines changed

.github/workflows/pypi.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Publish package to PyPI
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
jobs:
7+
build-n-publish:
8+
name: Build and publish package to PyPI
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v2
12+
- name: Set up Python
13+
uses: actions/setup-python@v1
14+
with:
15+
python-version: "3.9"
16+
- name: Install poetry
17+
run: |
18+
curl -fsS -o get-poetry.py https://install.python-poetry.org
19+
python get-poetry.py -y
20+
- name: Publish
21+
env:
22+
PYPI_TOKEN: ${{ secrets.pypi_password }}
23+
run: |
24+
$HOME/.local/bin/poetry config pypi-token.pypi $PYPI_TOKEN
25+
$HOME/.local/bin/poetry build -f sdist
26+
$HOME/.local/bin/poetry publish

build.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
import shutil
3+
4+
from setuptools import Extension, Distribution
5+
import numpy
6+
from Cython.Build import cythonize
7+
from Cython.Distutils.build_ext import new_build_ext as cython_build_ext
8+
9+
10+
def build() -> None:
11+
# if running in RTD, skip compilation
12+
if os.environ.get("READTHEDOCS") == "True":
13+
return
14+
15+
extensions = [
16+
Extension(
17+
'pyobs_qhyccd.qhyccddriver',
18+
['pyobs_qhyccd/qhyccddriver.pyx'],
19+
library_dirs=['lib/usr/local/lib/'],
20+
libraries=['qhyccd', 'cfitsio'],
21+
include_dirs=[numpy.get_include()],
22+
extra_compile_args=['-fPIC']
23+
)
24+
]
25+
ext_modules = cythonize(extensions)
26+
27+
distribution = Distribution(
28+
{
29+
"name": "extended",
30+
"ext_modules": ext_modules,
31+
"cmdclass": {
32+
"build_ext": cython_build_ext,
33+
},
34+
}
35+
)
36+
37+
distribution.run_command("build_ext")
38+
39+
# copy to source
40+
build_ext_cmd = distribution.get_command_obj("build_ext")
41+
for ext in build_ext_cmd.extensions:
42+
filename = build_ext_cmd.get_ext_filename(ext.name)
43+
shutil.copyfile(os.path.join(build_ext_cmd.build_lib, filename), filename)
44+
45+
46+
if __name__ == "__main__":
47+
build()

lib/usr/local/include/qhyccd.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#include "stdint.h"
66
#include "config.h"
77
#include <functional>
8-
8+
#include <string>
99

1010

1111

pyobs_qhyccd/libqhyccd.pxd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,7 @@ cdef extern from "../lib/usr/local/include/qhyccd.h":
100100
unsigned int GetQHYCCDMemLength(libusb_device_handle *handle)
101101
unsigned int GetQHYCCDSingleFrame(libusb_device_handle *handle,unsigned int *w, unsigned int *h,
102102
unsigned int *bpp, unsigned int *channels, unsigned char *imgdata)
103-
unsigned int CancelQHYCCDExposingAndReadout(libusb_device_handle *handle)
103+
unsigned int CancelQHYCCDExposingAndReadout(libusb_device_handle *handle)
104+
unsigned int GetQHYCCDExposureRemaining(libusb_device_handle *handle)
105+
unsigned int GetQHYCCDCameraStatus(libusb_device_handle *handle, unsigned char *buf);
106+
void SetQHYCCDLogLevel(unsigned int logLevel);

pyobs_qhyccd/qhyccdcamera.py

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import asyncio
2+
import logging
3+
import math
4+
import time
5+
from datetime import datetime
6+
from typing import Tuple, Any, Optional, Dict, List
7+
import numpy as np
8+
9+
from pyobs.interfaces import ICamera, IWindow, IBinning, ICooling, IAbortable
10+
from pyobs.modules.camera.basecamera import BaseCamera
11+
from pyobs.images import Image
12+
from pyobs.utils.enums import ExposureStatus
13+
from pyobs.utils.parallel import event_wait
14+
15+
from .qhyccddriver import QHYCCDDriver, Control, set_log_level
16+
17+
log = logging.getLogger(__name__)
18+
19+
20+
class QHYCCDCamera(BaseCamera, ICamera, IWindow, IBinning, IAbortable):
21+
"""A pyobs module for QHYCCD cameras."""
22+
23+
__module__ = "pyobs_qhyccd"
24+
25+
def __init__(self, **kwargs: Any):
26+
"""Initializes a new QHYCCDCamera.
27+
"""
28+
BaseCamera.__init__(self, **kwargs)
29+
30+
# driver
31+
self._driver: Optional[QHYCCDDriver] = None
32+
33+
# window and binning
34+
self._window = (0, 0, 0, 0)
35+
self._binning = (1, 1)
36+
37+
async def open(self) -> None:
38+
"""Open module."""
39+
await BaseCamera.open(self)
40+
41+
# disable logs
42+
set_log_level(0)
43+
44+
# get devices
45+
devices = QHYCCDDriver.list_devices()
46+
47+
# open camera
48+
self._driver = QHYCCDDriver(devices[0])
49+
self._driver.open()
50+
51+
# color cam?
52+
if self._driver.is_control_available(Control.CAM_COLOR):
53+
raise ValueError('Color cams are not supported.')
54+
55+
# usb traffic?
56+
if self._driver.is_control_available(Control.CONTROL_USBTRAFFIC):
57+
self._driver.set_param(Control.CONTROL_USBTRAFFIC, 60)
58+
59+
# gain?
60+
if self._driver.is_control_available(Control.CONTROL_GAIN):
61+
self._driver.set_param(Control.CONTROL_GAIN, 10)
62+
63+
# offset?
64+
if self._driver.is_control_available(Control.CONTROL_OFFSET):
65+
self._driver.set_param(Control.CONTROL_OFFSET, 140)
66+
67+
# bpp
68+
if self._driver.is_control_available(Control.CONTROL_TRANSFERBIT):
69+
self._driver.set_bits_mode(16)
70+
71+
# get full window
72+
self._window = self._driver.get_effective_area()
73+
74+
# set cooling
75+
#if self._temp_setpoint is not None:
76+
# await self.set_cooling(True, self._temp_setpoint)
77+
78+
async def close(self) -> None:
79+
"""Close the module."""
80+
await BaseCamera.close(self)
81+
82+
if self._driver:
83+
self._driver.close()
84+
85+
async def get_full_frame(self, **kwargs: Any) -> Tuple[int, int, int, int]:
86+
"""Returns full size of CCD.
87+
88+
Returns:
89+
Tuple with left, top, width, and height set.
90+
"""
91+
if self._driver is None:
92+
raise ValueError("No camera driver.")
93+
return self._driver.get_effective_area()
94+
95+
async def get_window(self, **kwargs: Any) -> Tuple[int, int, int, int]:
96+
"""Returns the camera window.
97+
98+
Returns:
99+
Tuple with left, top, width, and height set.
100+
"""
101+
return self._window
102+
103+
async def get_binning(self, **kwargs: Any) -> Tuple[int, int]:
104+
"""Returns the camera binning.
105+
106+
Returns:
107+
Tuple with x and y.
108+
"""
109+
return self._binning
110+
111+
async def set_window(self, left: int, top: int, width: int, height: int, **kwargs: Any) -> None:
112+
"""Set the camera window.
113+
114+
Args:
115+
left: X offset of window.
116+
top: Y offset of window.
117+
width: Width of window.
118+
height: Height of window.
119+
120+
Raises:
121+
ValueError: If binning could not be set.
122+
"""
123+
self._window = (left, top, width, height)
124+
log.info("Setting window to %dx%d at %d,%d...", width, height, left, top)
125+
126+
async def set_binning(self, x: int, y: int, **kwargs: Any) -> None:
127+
"""Set the camera binning.
128+
129+
Args:
130+
x: X binning.
131+
y: Y binning.
132+
133+
Raises:
134+
ValueError: If binning could not be set.
135+
"""
136+
self._binning = (x, y)
137+
log.info("Setting binning to %dx%d...", x, y)
138+
139+
async def list_binnings(self, **kwargs: Any) -> List[Tuple[int, int]]:
140+
"""List available binnings.
141+
142+
Returns:
143+
List of available binnings as (x, y) tuples.
144+
"""
145+
return [(1, 1), (2, 2), (3, 3), (4, 4)]
146+
147+
async def _expose(self, exposure_time: float, open_shutter: bool, abort_event: asyncio.Event) -> Image:
148+
"""Actually do the exposure, should be implemented by derived classes.
149+
150+
Args:
151+
exposure_time: The requested exposure time in seconds.
152+
open_shutter: Whether to open the shutter.
153+
abort_event: Event that gets triggered when exposure should be aborted.
154+
155+
Returns:
156+
The actual image.
157+
158+
Raises:
159+
GrabImageError: If exposure was not successful.
160+
"""
161+
# check driver
162+
if self._driver is None:
163+
raise ValueError("No camera driver.")
164+
165+
# set binning
166+
log.info("Set binning to %dx%d.", self._binning[0], self._binning[1])
167+
self._driver.set_bin_mode(*self._binning)
168+
169+
# set window, divide width/height by binning, from libfli:
170+
# "Note that the given lower-right coordinate must take into account the horizontal and
171+
# vertical bin factor settings, but the upper-left coordinate is absolute."
172+
width = int(math.floor(self._window[2]) / self._binning[0])
173+
height = int(math.floor(self._window[3]) / self._binning[1])
174+
log.info(
175+
"Set window to %dx%d (binned %dx%d) at %d,%d.",
176+
self._window[2],
177+
self._window[3],
178+
width,
179+
height,
180+
self._window[0],
181+
self._window[1],
182+
)
183+
self._driver.set_resolution(self._window[0], self._window[1], width, height)
184+
185+
# exposure time
186+
self._driver.set_param(Control.CONTROL_EXPOSURE, int(exposure_time * 1000.0))
187+
188+
# get date obs
189+
log.info(
190+
"Starting exposure with %s shutter for %.2f seconds...", "open" if open_shutter else "closed", exposure_time
191+
)
192+
date_obs = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")
193+
194+
# expose
195+
print("before expose", time.time())
196+
self._driver.expose_single_frame()
197+
print("after expose", time.time())
198+
199+
# wait for exposure
200+
if exposure_time > 0.5:
201+
await event_wait(abort_event, exposure_time - 0.5)
202+
#if abort_event.is_set():
203+
# raise
204+
205+
# get image
206+
print("before get", time.time())
207+
img = self._driver.get_single_frame()
208+
#loop = asyncio.get_running_loop()
209+
#img = await loop.run_in_executor(None, self._driver.get_single_frame)
210+
print("after get", time.time())
211+
212+
# wait exposure
213+
await self._wait_exposure(abort_event, exposure_time, open_shutter)
214+
215+
# create FITS image and set header
216+
image = Image(img)
217+
image.header["DATE-OBS"] = (date_obs, "Date and time of start of exposure")
218+
image.header["EXPTIME"] = (exposure_time, "Exposure time [s]")
219+
#image.header["DET-TEMP"] = (self._driver.get_temp(FliTemperature.CCD), "CCD temperature [C]")
220+
#image.header["DET-COOL"] = (self._driver.get_cooler_power(), "Cooler power [percent]")
221+
#image.header["DET-TSET"] = (self._temp_setpoint, "Cooler setpoint [C]")
222+
223+
# instrument and detector
224+
#image.header["INSTRUME"] = (self._driver.name, "Name of instrument")
225+
226+
# binning
227+
image.header["XBINNING"] = image.header["DET-BIN1"] = (self._binning[0], "Binning factor used on X axis")
228+
image.header["YBINNING"] = image.header["DET-BIN2"] = (self._binning[1], "Binning factor used on Y axis")
229+
230+
# window
231+
image.header["XORGSUBF"] = (self._window[0], "Subframe origin on X axis")
232+
image.header["YORGSUBF"] = (self._window[1], "Subframe origin on Y axis")
233+
234+
# statistics
235+
image.header["DATAMIN"] = (float(np.min(img)), "Minimum data value")
236+
image.header["DATAMAX"] = (float(np.max(img)), "Maximum data value")
237+
image.header["DATAMEAN"] = (float(np.mean(img)), "Mean data value")
238+
239+
# biassec/trimsec
240+
#full = self._driver.get_visible_frame()
241+
#self.set_biassec_trimsec(image.header, *full)
242+
243+
# return FITS image
244+
log.info("Readout finished.")
245+
return image
246+
247+
async def _wait_exposure(self, abort_event: asyncio.Event, exposure_time: float, open_shutter: bool) -> None:
248+
"""Wait for exposure to finish.
249+
250+
Params:
251+
abort_event: Event that aborts the exposure.
252+
exposure_time: Exp time in sec.
253+
"""
254+
255+
"""
256+
while True:
257+
# aborted?
258+
if abort_event.is_set():
259+
await self._change_exposure_status(ExposureStatus.IDLE)
260+
raise InterruptedError("Aborted exposure.")
261+
262+
# is exposure finished?
263+
if self._driver.is_exposing():
264+
break
265+
else:
266+
# sleep a little
267+
await asyncio.sleep(0.01)
268+
"""
269+
270+
async def _abort_exposure(self) -> None:
271+
"""Abort the running exposure. Should be implemented by derived class.
272+
273+
Raises:
274+
ValueError: If an error occured.
275+
"""
276+
if self._driver is None:
277+
raise ValueError("No camera driver.")
278+
#self._driver.cancel_exposure()
279+
280+
281+
__all__ = ["QHYCCDCamera"]

0 commit comments

Comments
 (0)