#if 0 V4L INDI Driver INDI Interface for V4L devices Copyright (C) 2003-2005 Jasem Mutlaq (mutlaqja@ikarustech.com) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #endif #include "v4ldriver.h" V4L_Driver::V4L_Driver() { V4LFrame = (img_t *) malloc (sizeof(img_t)); if (V4LFrame == NULL) { IDMessage(NULL, "Error: unable to initialize driver. Low memory."); IDLog("Error: unable to initialize driver. Low memory."); return; } camNameT[0].text = NULL; PortT[0].text = NULL; IUSaveText(&PortT[0], "/dev/video0"); divider = 128.; } V4L_Driver::~V4L_Driver() { free (V4LFrame); } void V4L_Driver::initProperties(const char *dev) { strncpy(device_name, dev, MAXINDIDEVICE); /* Connection */ fillSwitch(&PowerS[0], "CONNECT", "Connect", ISS_OFF); fillSwitch(&PowerS[1], "DISCONNECT", "Disconnect", ISS_ON); fillSwitchVector(&PowerSP, PowerS, NARRAY(PowerS), dev, "CONNECTION", "Connection", COMM_GROUP, IP_RW, ISR_1OFMANY, 60, IPS_IDLE); /* Port */ fillText(&PortT[0], "PORT", "Port", "/dev/ttyS0"); fillTextVector(&PortTP, PortT, NARRAY(PortT), dev, "DEVICE_PORT", "Ports", COMM_GROUP, IP_RW, 0, IPS_IDLE); /* Video Stream */ fillSwitch(&StreamS[0], "ON", "", ISS_OFF); fillSwitch(&StreamS[1], "OFF", "", ISS_ON); fillSwitchVector(&StreamSP, StreamS, NARRAY(StreamS), dev, "VIDEO_STREAM", "Video Stream", COMM_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Compression */ fillSwitch(&CompressS[0], "ON", "", ISS_ON); fillSwitch(&CompressS[1], "OFF", "", ISS_OFF); fillSwitchVector(&CompressSP, CompressS, NARRAY(StreamS), dev, "Compression", "", IMAGE_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Image type */ fillSwitch(&ImageTypeS[0], "Grey", "", ISS_ON); fillSwitch(&ImageTypeS[1], "Color", "", ISS_OFF); fillSwitchVector(&ImageTypeSP, ImageTypeS, NARRAY(ImageTypeS), dev, "Image Type", "", IMAGE_GROUP, IP_RW, ISR_1OFMANY, 0, IPS_IDLE); /* Camera Name */ fillText(&camNameT[0], "Model", "", ""); fillTextVector(&camNameTP, camNameT, NARRAY(camNameT), dev, "Camera Model", "", COMM_GROUP, IP_RO, 0, IPS_IDLE); /* Expose */ fillNumber(&ExposeTimeN[0], "EXPOSE_DURATION", "Duration (s)", "%5.2f", 0., 36000., 0.5, 1.); fillNumberVector(&ExposeTimeNP, ExposeTimeN, NARRAY(ExposeTimeN), dev, "CCD_EXPOSE_DURATION", "Expose", COMM_GROUP, IP_RW, 60, IPS_IDLE); /* Frame Rate */ fillNumber(&FrameRateN[0], "RATE", "Rate", "%0.f", 1., 50., 1., 10.); fillNumberVector(&FrameRateNP, FrameRateN, NARRAY(FrameRateN), dev, "FRAME_RATE", "Frame Rate", COMM_GROUP, IP_RW, 60, IPS_IDLE); /* Frame dimension */ fillNumber(&FrameN[0], "X", "X", "%.0f", 0., 0., 0., 0.); fillNumber(&FrameN[1], "Y", "Y", "%.0f", 0., 0., 0., 0.); fillNumber(&FrameN[2], "WIDTH", "Width", "%.0f", 0., 0., 10., 0.); fillNumber(&FrameN[3], "HEIGHT", "Height", "%.0f", 0., 0., 10., 0.); fillNumberVector(&FrameNP, FrameN, NARRAY(FrameN), dev, "CCD_FRAME", "Frame", IMAGE_GROUP, IP_RW, 60, IPS_IDLE); /*fillNumber(&ImageSizeN[0], "WIDTH", "Width", "%0.f", 0., 0., 10., 0.); fillNumber(&ImageSizeN[1], "HEIGHT", "Height", "%0.f", 0., 0., 10., 0.); fillNumberVector(&ImageSizeNP, ImageSizeN, NARRAY(ImageSizeN), dev, "IMAGE_SIZE", "Image Size", IMAGE_GROUP, IP_RW, 60, IPS_IDLE);*/ #ifndef HAVE_LINUX_VIDEODEV2_H fillNumber(&ImageAdjustN[0], "Contrast", "", "%0.f", 0., 256., 1., 0.); fillNumber(&ImageAdjustN[1], "Brightness", "", "%0.f", 0., 256., 1., 0.); fillNumber(&ImageAdjustN[2], "Hue", "", "%0.f", 0., 256., 1., 0.); fillNumber(&ImageAdjustN[3], "Color", "", "%0.f", 0., 256., 1., 0.); fillNumber(&ImageAdjustN[4], "Whiteness", "", "%0.f", 0., 256., 1., 0.); fillNumberVector(&ImageAdjustNP, ImageAdjustN, NARRAY(ImageAdjustN), dev, "Image Adjustments", "", IMAGE_GROUP, IP_RW, 60, IPS_IDLE); #else fillNumberVector(&ImageAdjustNP, NULL, 0, dev, "Image Adjustments", "", IMAGE_GROUP, IP_RW, 60, IPS_IDLE); #endif // We need to setup the BLOB (Binary Large Object) below. Using this property, we can send FITS to our client strcpy(imageB.name, "CCD1"); strcpy(imageB.label, "Feed"); strcpy(imageB.format, ""); imageB.blob = 0; imageB.bloblen = 0; imageB.size = 0; imageB.bvp = 0; imageB.aux0 = 0; imageB.aux1 = 0; imageB.aux2 = 0; strcpy(imageBP.device, dev); strcpy(imageBP.name, "Video"); strcpy(imageBP.label, "Video"); strcpy(imageBP.group, COMM_GROUP); strcpy(imageBP.timestamp, ""); imageBP.p = IP_RO; imageBP.timeout = 0; imageBP.s = IPS_IDLE; imageBP.bp = &imageB; imageBP.nbp = 1; imageBP.aux = 0; } void V4L_Driver::initCamBase() { #ifndef HAVE_LINUX_VIDEODEV2_H v4l_base = new V4L1_Base(); #else v4l_base = new V4L2_Base(); #endif } void V4L_Driver::ISGetProperties (const char *dev) { if (dev && strcmp (device_name, dev)) return; /* COMM_GROUP */ IDDefSwitch(&PowerSP, NULL); IDDefText(&PortTP, NULL); IDDefText(&camNameTP, NULL); IDDefSwitch(&StreamSP, NULL); #ifndef HAVE_LINUX_VIDEODEV2_H IDDefNumber(&FrameRateNP, NULL); #endif IDDefNumber(&ExposeTimeNP, NULL); IDDefBLOB(&imageBP, NULL); /* Image properties */ IDDefSwitch(&CompressSP, NULL); IDDefSwitch(&ImageTypeSP, NULL); IDDefNumber(&FrameNP, NULL); #ifndef HAVE_LINUX_VIDEODEV2_H IDDefNumber(&ImageAdjustNP, NULL); #endif } void V4L_Driver::ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n) { char errmsg[ERRMSGSIZ]; /* ignore if not ours */ if (dev && strcmp (device_name, dev)) return; /* Connection */ if (!strcmp (name, PowerSP.name)) { IUResetSwitches(&PowerSP); IUUpdateSwitches(&PowerSP, states, names, n); connectCamera(); return; } /* Compression */ if (!strcmp(name, CompressSP.name)) { IUResetSwitches(&CompressSP); IUUpdateSwitches(&CompressSP, states, names, n); CompressSP.s = IPS_OK; IDSetSwitch(&CompressSP, NULL); return; } /* Image Type */ if (!strcmp(name, ImageTypeSP.name)) { IUResetSwitches(&ImageTypeSP); IUUpdateSwitches(&ImageTypeSP, states, names, n); ImageTypeSP.s = IPS_OK; IDSetSwitch(&ImageTypeSP, NULL); return; } /* Video Stream */ if (!strcmp(name, StreamSP.name)) { if (checkPowerS(&StreamSP)) return; IUResetSwitches(&StreamSP); IUUpdateSwitches(&StreamSP, states, names, n); StreamSP.s = IPS_IDLE; if (StreamS[0].s == ISS_ON) { frameCount = 0; IDLog("Starting the video stream.\n"); v4l_base->start_capturing(errmsg); StreamSP.s = IPS_BUSY; } else { IDLog("The video stream has been disabled. Frame count %d\n", frameCount); v4l_base->stop_capturing(errmsg); } IDSetSwitch(&StreamSP, NULL); return; } } void V4L_Driver::ISNewText (const char *dev, const char *name, char *texts[], char *names[], int /*n*/) { IText *tp; /* ignore if not ours */ if (dev && strcmp (device_name, dev)) return; if (!strcmp(name, PortTP.name) ) { PortTP.s = IPS_OK; tp = IUFindText( &PortTP, names[0] ); if (!tp) return; IUSaveText(tp, texts[0]); IDSetText (&PortTP, NULL); return; } } void V4L_Driver::ISNewNumber (const char *dev, const char *name, double values[], char *names[], int n) { char errmsg[ERRMSGSIZ]; /* ignore if not ours */ if (dev && strcmp (device_name, dev)) return; /* Frame Size */ if (!strcmp (FrameNP.name, name)) { if (checkPowerN(&FrameNP)) return; int oldW = (int) FrameN[2].value; int oldH = (int) FrameN[3].value; FrameNP.s = IPS_OK; if (IUUpdateNumbers(&FrameNP, values, names, n) < 0) return; if (v4l_base->setSize( (int) FrameN[2].value, (int) FrameN[3].value) != -1) { FrameN[2].value = v4l_base->getWidth(); FrameN[3].value = v4l_base->getHeight(); IDSetNumber(&FrameNP, NULL); return; } else { FrameN[2].value = oldW; FrameN[3].value = oldH; FrameNP.s = IPS_ALERT; IDSetNumber(&FrameNP, "Failed to set a new image size."); } return; } #ifndef HAVE_LINUX_VIDEODEV2_H /* Frame rate */ if (!strcmp (FrameRateNP.name, name)) { if (checkPowerN(&FrameRateNP)) return; FrameRateNP.s = IPS_IDLE; if (IUUpdateNumbers(&FrameRateNP, values, names, n) < 0) return; v4l_base->setFPS( (int) FrameRateN[0].value ); FrameRateNP.s = IPS_OK; IDSetNumber(&FrameRateNP, NULL); return; } #endif if (!strcmp (ImageAdjustNP.name, name)) { if (checkPowerN(&ImageAdjustNP)) return; ImageAdjustNP.s = IPS_IDLE; if (IUUpdateNumbers(&ImageAdjustNP, values, names, n) < 0) return; #ifndef HAVE_LINUX_VIDEODEV2_H v4l_base->setContrast( (int) (ImageAdjustN[0].value * divider)); v4l_base->setBrightness( (int) (ImageAdjustN[1].value * divider)); v4l_base->setHue( (int) (ImageAdjustN[2].value * divider)); v4l_base->setColor( (int) (ImageAdjustN[3].value * divider)); v4l_base->setWhiteness( (int) (ImageAdjustN[4].value * divider)); ImageAdjustN[0].value = v4l_base->getContrast() / divider; ImageAdjustN[1].value = v4l_base->getBrightness() / divider; ImageAdjustN[2].value = v4l_base->getHue() / divider; ImageAdjustN[3].value = v4l_base->getColor() / divider; ImageAdjustN[4].value = v4l_base->getWhiteness() / divider; #else unsigned int ctrl_id; for (int i=0; i < ImageAdjustNP.nnp; i++) { ctrl_id = *((unsigned int *) ImageAdjustNP.np[i].aux0); if (v4l_base->setINTControl( ctrl_id , ImageAdjustNP.np[i].value, errmsg) < 0) { ImageAdjustNP.s = IPS_ALERT; IDSetNumber(&ImageAdjustNP, "Unable to adjust setting. %s", errmsg); return; } } #endif ImageAdjustNP.s = IPS_OK; IDSetNumber(&ImageAdjustNP, NULL); return; } /* Exposure */ if (!strcmp (ExposeTimeNP.name, name)) { if (checkPowerN(&ExposeTimeNP)) return; if (StreamS[0].s == ISS_ON) v4l_base->stop_capturing(errmsg); StreamS[0].s = ISS_OFF; StreamS[1].s = ISS_ON; StreamSP.s = IPS_IDLE; IDSetSwitch(&StreamSP, NULL); V4LFrame->expose = 1000; v4l_base->start_capturing(errmsg); ExposeTimeNP.s = IPS_BUSY; IDSetNumber(&ExposeTimeNP, NULL); return; } } void V4L_Driver::newFrame(void *p) { ((V4L_Driver *) (p))->updateFrame(); } void V4L_Driver::updateFrame() { char errmsg[ERRMSGSIZ]; static int dropLarge = 3; if (StreamSP.s == IPS_BUSY) { frameCount++; // Drop some frames if (FrameN[2].value > 160) { dropLarge--; if (dropLarge == 0) { dropLarge = 3; return; } else if (dropLarge < 2) return; } updateStream(); } else if (ExposeTimeNP.s == IPS_BUSY) { V4LFrame->Y = v4l_base->getY(); v4l_base->stop_capturing(errmsg); grabImage(); } } void V4L_Driver::updateStream() { int width = v4l_base->getWidth(); int height = v4l_base->getHeight(); uLongf compressedBytes = 0; uLong totalBytes; unsigned char *targetFrame; int r; if (PowerS[0].s == ISS_OFF || StreamS[0].s == ISS_OFF) return; if (ImageTypeS[0].s == ISS_ON) V4LFrame->Y = v4l_base->getY(); else V4LFrame->colorBuffer = v4l_base->getColorBuffer(); totalBytes = ImageTypeS[0].s == ISS_ON ? width * height : width * height * 4; targetFrame = ImageTypeS[0].s == ISS_ON ? V4LFrame->Y : V4LFrame->colorBuffer; /* Do we want to compress ? */ if (CompressS[0].s == ISS_ON) { /* Compress frame */ V4LFrame->compressedFrame = (unsigned char *) realloc (V4LFrame->compressedFrame, sizeof(unsigned char) * totalBytes + totalBytes / 64 + 16 + 3); compressedBytes = sizeof(unsigned char) * totalBytes + totalBytes / 64 + 16 + 3; r = compress2(V4LFrame->compressedFrame, &compressedBytes, targetFrame, totalBytes, 4); if (r != Z_OK) { /* this should NEVER happen */ IDLog("internal error - compression failed: %d\n", r); return; } /* #3.A Send it compressed */ imageB.blob = V4LFrame->compressedFrame; imageB.bloblen = compressedBytes; imageB.size = totalBytes; strcpy(imageB.format, ".stream.z"); } else { /* #3.B Send it uncompressed */ imageB.blob = targetFrame; imageB.bloblen = totalBytes; imageB.size = totalBytes; strcpy(imageB.format, ".stream"); } imageBP.s = IPS_OK; IDSetBLOB (&imageBP, NULL); #ifndef HAVE_LINUX_VIDEODEV2_H char errmsg[ERRMSGSIZ]; v4l_base->start_capturing(errmsg); #endif } /* Downloads the image from the CCD row by row and store them in a raw file. N.B. No processing is done on the image */ int V4L_Driver::grabImage() { int err, fd; char errmsg[ERRMSG_SIZE]; char filename[] = "/tmp/fitsXXXXXX"; if ((fd = mkstemp(filename)) < 0) { IDMessage(device_name, "Error making temporary filename."); IDLog("Error making temporary filename.\n"); return -1; } close(fd); err = writeFITS(filename, errmsg); if (err) { IDMessage(device_name, errmsg, NULL); return -1; } return 0; } int V4L_Driver::writeFITS(const char * filename, char errmsg[]) { FITS_FILE* ofp; int i, bpp, bpsl, width, height; long nbytes; FITS_HDU_LIST *hdu; ofp = fits_open (filename, "w"); if (!ofp) { snprintf(errmsg, ERRMSG_SIZE, "Error: cannot open file for writing."); return (-1); } width = v4l_base->getWidth(); height = v4l_base->getHeight(); bpp = 1; /* Bytes per Pixel */ bpsl = bpp * width; /* Bytes per Line */ nbytes = 0; hdu = create_fits_header (ofp, width, height, bpp); if (hdu == NULL) { snprintf(errmsg, ERRMSG_SIZE, "Error: creating FITS header failed."); return (-1); } if (fits_write_header (ofp, hdu) < 0) { snprintf(errmsg, ERRMSG_SIZE, "Error: writing to FITS header failed."); return (-1); } for (i= height - 1; i >=0 ; i--) { fwrite(V4LFrame->Y + (i * width), 1, width, ofp->fp); nbytes += bpsl; } nbytes = nbytes % FITS_RECORD_SIZE; if (nbytes) { while (nbytes++ < FITS_RECORD_SIZE) putc (0, ofp->fp); } if (ferror (ofp->fp)) { snprintf(errmsg, ERRMSG_SIZE, "Error: write error occured"); return (-1); } fits_close (ofp); /* Success */ ExposeTimeNP.s = IPS_OK; IDSetNumber(&ExposeTimeNP, NULL); uploadFile(filename); return 0; } void V4L_Driver::uploadFile(const char * filename) { FILE * fitsFile; unsigned char *fitsData; int r=0; unsigned int nr = 0; uLong totalBytes; uLongf compressedBytes = 0; struct stat stat_p; if ( -1 == stat (filename, &stat_p)) { IDLog(" Error occoured attempting to stat %s\n", filename); return; } totalBytes = stat_p.st_size; fitsData = new unsigned char[totalBytes]; fitsFile = fopen(filename, "r"); if (fitsFile == NULL) return; /* #1 Read file from disk */ for (unsigned int i=0; i < totalBytes; i+= nr) { nr = fread(fitsData + i, 1, totalBytes - i, fitsFile); if (nr <= 0) { IDLog("Error reading temporary FITS file.\n"); return; } } if (CompressS[0].s == ISS_ON) { /* #2 Compress it */ V4LFrame->compressedFrame = (unsigned char *) realloc (V4LFrame->compressedFrame, sizeof(unsigned char) * totalBytes + totalBytes / 64 + 16 + 3); compressedBytes = sizeof(unsigned char) * totalBytes + totalBytes / 64 + 16 + 3; r = compress2(V4LFrame->compressedFrame, &compressedBytes, fitsData, totalBytes, 9); if (r != Z_OK) { /* this should NEVER happen */ IDLog("internal error - compression failed: %d\n", r); return; } /* #3.A Send it compressed */ imageB.blob = V4LFrame->compressedFrame; imageB.bloblen = compressedBytes; imageB.size = totalBytes; strcpy(imageB.format, ".fits.z"); } else { imageB.blob = fitsData; imageB.bloblen = totalBytes; imageB.size = totalBytes; strcpy(imageB.format, ".fits"); } imageBP.s = IPS_OK; IDSetBLOB (&imageBP, NULL); delete (fitsData); } void V4L_Driver::connectCamera() { char errmsg[ERRMSGSIZ]; switch (PowerS[0].s) { case ISS_ON: if (v4l_base->connectCam(PortT[0].text, errmsg) < 0) { PowerSP.s = IPS_IDLE; PowerS[0].s = ISS_OFF; PowerS[1].s = ISS_ON; IDSetSwitch(&PowerSP, "Error: unable to open device"); IDLog("Error: %s\n", errmsg); return; } /* Sucess! */ PowerS[0].s = ISS_ON; PowerS[1].s = ISS_OFF; PowerSP.s = IPS_OK; IDSetSwitch(&PowerSP, "Video4Linux Generic Device is online. Retrieving basic data."); v4l_base->registerCallback(newFrame, this); V4LFrame->compressedFrame = (unsigned char *) malloc (sizeof(unsigned char) * 1); IDLog("V4L Device is online. Retrieving basic data.\n"); getBasicData(); break; case ISS_OFF: PowerS[0].s = ISS_OFF; PowerS[1].s = ISS_ON; PowerSP.s = IPS_IDLE; free(V4LFrame->compressedFrame); V4LFrame->compressedFrame = NULL; v4l_base->disconnectCam(); IDSetSwitch(&PowerSP, "Video4Linux Generic Device is offline."); break; } } /* Retrieves basic data from the device upon connection.*/ void V4L_Driver::getBasicData() { int xmax, ymax, xmin, ymin; v4l_base->getMaxMinSize(xmax, ymax, xmin, ymin); /* Width */ FrameN[2].value = v4l_base->getWidth(); FrameN[2].min = xmin; FrameN[2].max = xmax; /* Height */ FrameN[3].value = v4l_base->getHeight(); FrameN[3].min = ymin; FrameN[3].max = ymax; IUUpdateMinMax(&FrameNP); IDSetNumber(&FrameNP, NULL); IUSaveText(&camNameT[0], v4l_base->getDeviceName()); IDSetText(&camNameTP, NULL); #ifndef HAVE_LINUX_VIDEODEV2_H updateV4L1Controls(); #else updateV4L2Controls(); #endif } #ifdef HAVE_LINUX_VIDEODEV2_H void V4L_Driver::updateV4L2Controls() { // #1 Query for INTEGER controls, and fill up the structure free(ImageAdjustNP.np); ImageAdjustNP.nnp = 0; if (v4l_base->queryINTControls(&ImageAdjustNP) > 0) IDDefNumber(&ImageAdjustNP, NULL); } #else void V4L_Driver::updateV4L1Controls() { if ( (v4l_base->getContrast() / divider) > ImageAdjustN[0].max) divider *=2; if ( (v4l_base->getHue() / divider) > ImageAdjustN[2].max) divider *=2; ImageAdjustN[0].value = v4l_base->getContrast() / divider; ImageAdjustN[1].value = v4l_base->getBrightness() / divider; ImageAdjustN[2].value = v4l_base->getHue() / divider; ImageAdjustN[3].value = v4l_base->getColor() / divider; ImageAdjustN[4].value = v4l_base->getWhiteness() / divider; ImageAdjustNP.s = IPS_OK; IDSetNumber(&ImageAdjustNP, NULL); } #endif int V4L_Driver::checkPowerS(ISwitchVectorProperty *sp) { if (PowerSP.s != IPS_OK) { if (!strcmp(sp->label, "")) IDMessage (device_name, "Cannot change property %s while the camera is offline.", sp->name); else IDMessage (device_name, "Cannot change property %s while the camera is offline.", sp->label); sp->s = IPS_IDLE; IDSetSwitch(sp, NULL); return -1; } return 0; } int V4L_Driver::checkPowerN(INumberVectorProperty *np) { if (PowerSP.s != IPS_OK) { if (!strcmp(np->label, "")) IDMessage (device_name, "Cannot change property %s while the camera is offline.", np->name); else IDMessage (device_name, "Cannot change property %s while the camera is offline.", np->label); np->s = IPS_IDLE; IDSetNumber(np, NULL); return -1; } return 0; } int V4L_Driver::checkPowerT(ITextVectorProperty *tp) { if (PowerSP.s != IPS_OK) { if (!strcmp(tp->label, "")) IDMessage (device_name, "Cannot change property %s while the camera is offline.", tp->name); else IDMessage (device_name, "Cannot change property %s while the camera is offline.", tp->label); tp->s = IPS_IDLE; IDSetText(tp, NULL); return -1; } return 0; } FITS_HDU_LIST * V4L_Driver::create_fits_header (FITS_FILE *ofp, uint width, uint height, uint bpp) { FITS_HDU_LIST *hdulist; char expose_s[80]; char obsDate[80]; char instrumentName[80]; char ts[32]; struct tm *tp; time_t t; time (&t); tp = gmtime (&t); strftime (ts, sizeof(ts), "%Y-%m-%dT%H:%M:%S", tp); snprintf(instrumentName, 80, "INSTRUME= '%s'", v4l_base->getDeviceName()); snprintf(obsDate, 80, "DATE-OBS= '%s' /Observation Date UTC", ts); hdulist = fits_add_hdu (ofp); if (hdulist == NULL) return (NULL); hdulist->used.simple = 1; hdulist->bitpix = 8; hdulist->naxis = 2; hdulist->naxisn[0] = width; hdulist->naxisn[1] = height; hdulist->naxisn[2] = bpp; hdulist->used.datamin = 0; hdulist->used.datamax = 0; hdulist->used.bzero = 1; hdulist->bzero = 0.0; hdulist->used.bscale = 1; hdulist->bscale = 1.0; snprintf(expose_s, sizeof(expose_s), "EXPOSURE= %d / milliseconds", V4LFrame->expose); fits_add_card (hdulist, expose_s); fits_add_card (hdulist, instrumentName); fits_add_card (hdulist, obsDate); return (hdulist); }